There are some constraints on trying to use all these features together: an optional block must be the last parameter; an implicit array must follow any parameters other than an optional block; and key/value pairs will only be collected into a hash if they are the last parameters in a method invocation. The first invocation below is legal because the key/value pairs are the last parameters, but the second invocation is illegal because they are followed by other parameters, and Ruby can't determine which ones should be gathered into the array and which ones should be gathered into the hash:
book = Book.new( 'War and Peace',
:author => 'Tolstoy',
:isbn => '0375760644') # legal!
book = Book.new( 'War and Peace',
:author => 'Tolstoy',
:isbn => '0375760644',
'Steve', 'Amanda', 'Marty') # not legal!
Using these features we can create flexible, expressive method definitions and invocations, but Ruby still has a few more features that are useful when we're constructing DSLs -- eval, class_eval and instance_eval.
Fundamentally, eval lets us take a string and evaluate it as Ruby statements. (Eval also supports varying the context in which this is done, but we won't cover that in this column.) #eval is a method on Kernel, so we can use it inside objects or in simple scripts.
Here's an example:
eval -2 + 2" # => 4
This can be very useful if we want to execute arbitrary strings that are being constructed dynamically, or are being read from some external location, such as a file.
#instance_eval is a public method on Object that takes either a string or a block as a parameter and executes the parameter in the context of the receiver. When the string or the block is evaluated, self is set to the receiver of instance_eval. This means that instance variables and private methods can be accessed within the block -- though this can be both succinct and dangerous, since it breaks the encapsulation of the receiver! Let's have a look at an alternative implementation of our book matching code that uses instance_eval:
class Book
def initialize(title, details = {}, *owners)
@title = title
@author = details[:author] || 'Unknown'
@isbn = details[:isbn]
@owners = owners
end
def matches(&block)
self.instance_eval &block
end
end
book = Book.new('War and Peace',
{:author =>'Tolstoy', :isbn => '0375760644'},
['Steve', 'Amanda', 'Marty'])
book.matches {@title == 'War and Peace'} # => true
Another common use for instance_eval is to define new methods at run time. In Ruby we can define methods that are unique for an instance, and this is what happens when we define a method using #instance_eval and the receiver is an instance:
book = Book.new('War and Peace',
{:author =>'Tolstoy', :isbn => '0375760644'},
['Steve', 'Amanda', 'Marty'])
book.instance_eval do
def location
"Russia"
end
end
book.location # => "Russia"
book2 = Book.new('Lord of the Rings',
{:author =>'Tolkien'},
['Jonathan'])
book2.location # => undefined method `location'
When we do the same thing to a class, the same rules apply -- in this case, the receiver is a named instance of Class, and the newly defined methods are Class methods, but available only on that specific instance, which is equivalent to creating a new (named) class method. This sounds confusing because the word "class" is overloaded, so let's look at an example:
book.class.instance_eval do
def content_description
"Book class"
end
end
Book.content_description # => "Book class"
book.content_description # => undefined method `content_description'
Here we're invoking #instance_eval on the class Book, and the result is a class method on Book, which cannot be invoked on the instance book.
The idea behind #class_eval is similar to the idea behind #instance_eval, but #class_eval is only implemented on Class, not on Object. When we invoke a method in the context of a #class_eval, the corresponding class method is executed. When we define a method in the context of a #class_eval, a new instance method is created. Here are some examples:
book.class.class_eval do
def location
"Russia"
end
end
book.location # => "Russia"
book2 = Book.new('Lord of the Rings',
{:author => 'Tolkien'},
['Steve', 'Amanda', 'Marty'])
book2.location # => "Russia"
The first invocation of #class_eval creates a new instance method on the class Book, so we can ask both book and book2 for their locations (though the implementation and the answer are a bit simplistic). We can also invoke the class method #content_description, defined earlier using #instance_eval on a class, as shown below:
book.class.instance_eval do
def content_description
"Book class"
end
end
book.class.class_eval "content_description" # => "Book class"
book.class.instance_eval "content_description" # => "Book class"
Finally, let's put all this together to create a DSL for creating a library and the books within that library. Here's an example of using the DSL:
library = Library.new do
new_book :title => 'War and Peace',
:author => 'Tolstoy',
:isbn => '0375760644',
:owners => ['Amanda']
new_book :title => 'Lord of the Rings',
:author => 'Tolkien',
:owners => ['Steve', 'Amanda', 'Marty']
end
and here's the content of the library instance after we execute this code:
#<Library:0x8b560
@books=
[#<Book:0x8b3f8
@author="Tolstoy",
@isbn="0375760644",
@owners=["Amanda"],
@title="War and Peace">,
#<Book:0x8b358
@author="Tolkien",
@isbn=nil,
@owners=["Steve", "Amanda", "Marty"],
@title="Lord of the Rings">]>
So what does the implementation of the DSL look like? Clearly we need a way to create a book, so let's use one of our earlier implementations (which one isn't terribly important):
class Book
def initialize(title, details = {}, owners = [])
@title = title
@author = details[:author] || 'Unknown'
@isbn = details[:isbn]
@owners = owners
end
end
Now we need to write our library class. We want to execute a block of code when we create the library, so we need to pass an optional block to #initialize. We want to be able to access a method called #new_book in that block; to keep the DSL clean, we don't want to need to specify the receiver of the block, so we want the receiver to implicitly be the library. This points us to using #instance_eval to evaluate the block, which leads to something like this:
class Library
def initialize(&block)
@books = []
self.instance_eval(&block)
end
def new_book(parameters)
title = parameters.delete(:title)
owners = parameters.delete(:owners)
@books << Book.new(title, parameters, owners)
end
end
The important thing here isn't really the details of the implementation, it's that Ruby gives us the features and flexibility that we need to create a language that clearly expresses the details of a specific domain, without a lot of the clutter and baggage that accompanies many programming languages. I hope that these examples encourage you to experiment with creating your own DSLs, and to learn more about features of Ruby that go beyond what you might have encountered in your current programming language.
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.
Offensive content reportedSo 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)
2
selasie - 10/12/07
For sometime now i've been programming in ruby
» Report offensive content