In my first column, I mentioned briefly that Ruby's flexible syntax made it particularly suitable for building domain-specific languages (DSLs).
This month, I'm going to expand on how we define and invoke Ruby methods, and also show you how to use Ruby meta-programming to instruct your DSL to create new classes on the fly, so that you have most of the tools you need for DSL generation.
We've already seen that we create objects in Ruby by sending #new to a class:
book = Book.new
When a class receives #new it allocates space for a new object and then, if the method exists, invokes the #initialize instance method to put the new object in a valid state. Any arguments passed to #new are automatically passed to initialize, like this:
class Book
def initialize(title, author, isbn)
@title = title
@author = author
@isbn = isbn
end
end
book = Book.new('War and Peace', 'Tolstoy', '0375760644')
This implementation forces us to supply the title, the author and the ISBN every time we create a book -- if we don't know the details, we still need to provide some values. For example, if all I know is a title, I'm forced to do something like this:
book = Book.new('To Serve Them All My Days', 'Unknown', '')
This is a bit cumbersome, and it also leaves the encoding of unknown values to the whim of each developer. It would be nice if there was a way to provide default values for the parameters I don't know, and indeed, in Ruby there is. Here's a new class definition that forces us to supply a title, but provides default values for author and ISBN:
class Book
def initialize(title, author='Unknown', isbn=nil)
@title = title
@author = author
@isbn = isbn
end
end
When a method has default values for parameters, the corresponding parameters can be omitted; since parameters are mapped by position, this means that if you want to let one parameter use its default, you have to accept the defaults for all the following parameters as well:
book = Book.new('War and Peace', 'Tolstoy', '0375760644') #valid
book = Book.new('War and Peace', 'Tolstoy') #valid
book = Book.new('War and Peace') #valid
book = Book.new('War and Peace', '0375760644') #valid syntax, but bad semantics
The last example will compile and execute, but it will create a book with an author of "0375760644" and an ISBN of nil, which probably isn't what was intended.
Now let's imagine that we also want to list the people who own a book when we create the book, and that this list is arbitrarily long. The obvious solution is to pass the authors as an array, like this:
class Book
def initialize(title, author='Unknown', isbn=nil, owners=[])
@title = title
@author = author
@isbn = isbn
@owners = owners
end
end
book = Book.new('War and Peace', 'Tolstoy', '0375760644', ['Steve', 'Amanda', 'Marty'])
This certainly works, but Ruby gives us an easier way. Here's something that's semantically equivalent:
class Book
def initialize(title, author='Unknown', isbn=nil, *owners)
@title = title
@author = author
@isbn = isbn
@owners = owners
end
end
book = Book.new('War and Peace', 'Tolstoy', '0375760644', 'Steve', 'Amanda', 'Marty')
When we invoke #new in this case, we don't need to make the beginnings of the owners array explicit -- specifying "*owners" in the method definition tells Ruby to pick up any remaining arguments and put them in an array called owners. The optional array argument has an implicit default value of an empty Array ([]); it's illegal to try to set an explicit default value for an optional array argument.
Any method definition may also include one other optional argument -- a block of code that can be invoked within the method. We've already seen this when we listed examples of enumeration methods in an earlier column:
(1..5).each { |n| puts n*n }
In this code the {} denote the (nominally) optional block. Let's show how you would define a method like this by adding a method to Book that returns a boolean indicating if the book matches some conditions contained in a block. Here's how we might invoke the method:
book.matches { |book| book.title == 'War and Peace' }
and here's the implementation:
class Book
# accessors and other details omitted
def matches(&block)
yield(self)
end
end
The "&" in &block indicates that this is an optional block argument, and the #yield tells Ruby to yield control to any optional block parameter, passing self as a parameter. The following code is logically equivalent, but demonstrates two other ways of accomplishing the same things:
class Book
# accessors and other details omitted
def matches(&block)
block.call(self)
end
end
book.matches do |book|
book.title == 'War and Peace'
end
Ruby also has two more tricks up its sleeve that can improve the expressiveness of our code. First, parentheses are usually optional in method invocations. While programmers have come to accept the proliferation of parentheses, square brackets and braces in their programming languages, these characters aren't a common part of natural language, and removing them makes code more readable. Finally, Ruby is very flexible in the way it treats hashes as method arguments.
A hash, as you may recall, is a set of key value pairs, and we create literal hashes like this:
{ :first_name => 'Nelson', :last_name => 'Mandela' }
Using hashes gives us a way to provide named parameters. Let's change our Book class to use a hash instead of individual parameters for author and ISBN when we create a new instance.
class Book
def initialize(title, details = {}, *owners)
@title = title
@author = details[:author] || 'Unknown'
@isbn = details[:isbn]
@owners = owners
end
end
book = Book.new('War and Peace',
{:author => 'Tolstoy', :isbn => '0375760644'},
'Steve', 'Amanda', 'Marty')
This is the literal use of a hash -- we still don't need to specify arguments that we don't need, we can specify the arguments inside the hash in any order we like, and we can use the hash keys to make the method invocation more expressive. If we don't have any optional arguments that need to be collected into an array, Ruby doesn't even need us to put in the {} delimiters; it will collect up any key value pairs at the end of the method invocation and put them into a hash. So this is a completely legal constructor:
book = Book.new('War and Peace', :author => 'Tolstoy', :isbn => '0375760644')
Do you need help with Ruby? 



1
Antony - 19/11/07
Following the code above, the owner array passed into the library books contained itself an array containing the array of names.
So library.books[1].owners[1] returned nil.
Was this the intention?
When i changed as below it worked more like I expected it.
library.books[1].owners[1] returned Amanda.
Why is that? (I'm a newbie)
» Report offensive content
2
selasie - 10/12/07
For sometime now i've been programming in ruby
» Report offensive content