So far the only built-in type that we've worked with has been String, so let's look at some of the other common types. Ruby has extensive support for arrays, and for hashes/maps. Literal arrays are created with square brackets ([]), so a literal array of the first five integers looks like [1, 2, 3, 4, 5]. However, arrays aren't constrained to contain objects of the same type, so we could just as easily create an array like this: [1, 'ruby', 2.718, Magazine.new]. Arrays are accessed using square bracket notation, and array indexes start at 0:
# file: arrays.rb array = [1, 'ruby', 2.718, Magazine.new] array[0] # => 1 array[1] # => "ruby"
We can also create a literal hash, using curly braces ({}) to delimit the hash, and => to denote the key/value pairs. Keys aren't restricted in any way, so we can use any object as a key and any object as a value. The values are accessed in the same style as array elements.
# file: hashes.rb
hash = { 1 => 'one', 'message' => 'Hello World!'}
hash[1] # => "one"
hash['message'] # => "Hello World!"
Arrays and hashes are used extensively in Ruby, in part because it's so easy to iterate over their contents. The fundamental iteration method in Ruby is "each" -- it loops over the contents of a collection and performs some action on each element in the collection in turn, with the action defined by a construct called a block. Other languages have blocks, but Ruby makes it particularly easy to create a block right where we need it. For example, to print out the squares of the first five integers, we could use "each" like this:
[1, 2, 3, 4, 5].each { |n| puts n*n }
The code between the {}'s is the block, and the variable between ||'s is the iterator variable; each time the block is invoked the iterator variable contains an element from the collection, and the block is invoked once for each element. Although we've used an array for my example, "each" is implemented on a number of classes that can be treated as collections. When we iterate over a hash with "each", the iterator variable contains an array with the key and the value, so the following code also prints out the squares of the first five integers (though with a hash the order isn't guaranteed):
hash = { 1 => 'one', 2 => 'two', 3 => 'three', 4 => 'four', 5 => 'five' }
hash.each { |key_value| puts key_value[0] * key_value[0] }
Ruby also has a Range object, which also implements "each", so this is another way to do the same thing:
(1..5).each { |n| puts n*n }
Ruby builds on this standard approach to iteration with extensions contained in the module Enumeration (we'll talk more about modules in a later column). Enumeration implements the following methods (some of which have aliases):
- select / find_all : return a collection with all the elements that match the boolean condition in the block
- collect / map : return a collection containing the results of performing the block on each element
- detect / find : return the first element that matches the boolean condition in the block
- reject : return a collection with all the elements that don't match the boolean condition in the block
- any? : return true if at least one element matches the boolean condition in the block
- all? : return true if every element matches the boolean condition in the block
Here are simple examples of each of these methods:
(1..5).select {|n| n > 2} # => [3, 4, 5]
(1..5).find_all {|n| n > 2} # => [3, 4, 5]
(1..5).collect {|n| n*n } # => [1, 4, 9, 16, 25]
(1..5).map {|n| n*n } # => [1, 4, 9, 16, 25]
(1..5).detect {|n| n > 2} # => 3
(1..5).find {|n| n > 2} # => 3
(1..5).reject {|n| n > 2} # => [1, 2]
(1..5).any? {|n| n > 2} # => true
(1..5).all? {|n| n > 2} # => false
You should be able to see by now that it's quick to get started in Ruby, because there's very little syntactic overhead, and the base libraries provide a lot of useful functionality that you don't need to write yourself. Ruby's expressive and flexible syntax also make it popular for creating domain specific languages (DSLs), and to a large extent Ruby on Rails is a DSL for building Web applications. Ruby method definitions can include default values for optional parameters, arbitrary numbers of parameters (which are collected into arrays), and parameters that are passed as hashmaps with named keys -- all of these make it easier to create a DSL that closely mimics the language of the domain, at least once you have a little practice.
We don't have the time or space to go into the details of Ruby on Rails, but we can take a quick look at some examples of ActiveRecord, the persistence framework bundled with Rails. If we have a database that contains the tables Libraries and Books, where a library contains many books and every book must be part of a library, then we can represent these tables and relationships like this:
class Library < ActiveRecord::Base has_many :books, :dependent => :destroy end class Book < ActiveRecord::Base belongs_to :library validates_presence_of :library end
Invoking these methods when the classes are defined creates all the accessors that we need to maintain these objects, with just the names that we'd expect, so we can say:
book.library = a_different_library
or
library.books << a_new_book
where << is the operator that adds a new element to a collection. Using ActiveRecord as a persistence DSL it's very easy to create a domain model that reflects the contents of your database, then extend it with the operations peculiar to the specific domain.
So in Ruby we have a dynamically typed, text-oriented, fully object oriented language with a flexible, expressive syntax that's readily amenable to the creation of domain specific languages, that is also accompanied by a powerful set of built-in libraries. Hopefully that ticks enough of the boxes on your language feature checklist for you to have a look at Ruby yourself, or at least look forward to my next column on Ruby!
References
- Ruby Language -- http://www.ruby-lang.org/en/
- Ruby One Click Installer for Windows -- http://rubyforge.org/projects/rubyinstaller/
- Ruby on Rails -- http://www.rubyonrails.org/
Do you need help with Ruby? 




