Many Ruby gems expose their API as a DSL instead of a set of classes. A popular example of this is RSpec, which lets you write tests like this:
describe User do
describe '#full_name' do
it 'should return the concatenated first and last names' do
user = User.new(:first_name => 'Henry', :last_name => 'Cook')
user.full_name.should == 'Henry Cook'
end
end
end
Under the hood RSpec converts the describe something do
syntax to plain Ruby classes and objects. However, RSpec never tells you the names of those magic classes and in fact they might not even have names. This makes it hard to find the right place for helper methods you'd like to use in your DSL blocks.
For the sake of this example, let's assume you'd like to extract the User.new(...)
invokation into a helper method new_user(...)
.
Don't do this (although it happens to work):
def new_user(first_name, last_name)
User.new(:first_name => first_name, :last_name => last_name)
end
describe User do
describe '#full_name' do
it 'should return the concatenated first and last names' do
user = new_user('Henry', 'Cook')
user.full_name.should == 'Henry Cook'
end
end
end
By defining a method in the void you are extending Object
, the mother of all Ruby classes. This means that every single object now has a new_user
method.
A more humble way to define that method is to use a lambda:
describe User do
new_user = lambda do |first_name, last_name|
User.new(:first_name => first_name, :last_name => last_name)
end
describe '#full_name' do
it 'should return the concatenated first and last names' do
user = instance_exec('Henry', 'Cook', &new_user)
user.full_name.should == 'Henry Cook'
end
end
end
Here new_user
is a local variable that happens to contain a method. You can invoke it with instance_exec. It's not visible to other classes but can still be captured by every code block below it – exactly what we need.
We use the lambda-technique to DRY up our Rails routes machinist blueprints, and, of course, RSpec examples.