Many times in Ruby, it is useful to allow the creator of an object to configure it using a block. This is especially true when writing a domain-specific language (DSL).

At work, I’ve built a Ruby DSL on top of Rake for building our C++ applications, and I use the configuration block idiom to allow a particular project to be customized:

Using a Configuration Block
project "myProject" do |p|
p.use_pthreads
p.add_compile_flags %w{-DMMAP_SUPPORT -march=i686}
p.link_with "m"
p.link_with %w{myLib1 myLib2}
end

To support this API, I have code something like:

Implementing a Configuration Block
def project(name, &block)
Project.new(name, &block)
end
class Project
def initialize(name)
@name = name
# ...
yield self if block_given?
# ...
end
def use_pthreads
#...
end
def add_compile_flags(flags)
#...
end
def link_with(libraries)
#...
end
end

This works fine, but I find the configuration block to be a bit more verbose than I’d like. There is a way to simplify the block:

Using a Simpler Configuration Block
project "myProject" do
use_pthreads
add_compile_flags %w{-DMMAP_SUPPORT -march=i686}
link_with "m"
link_with %w{myLib1 myLib2}
end

In this version, I no longer need to specify the p argument to the block or prefix all of the configuration method calls with p..

The code to make this work reaches deeper into the bag of Ruby tricks:

Implementing a Simpler Configuration Block
class Project
def initialize(name, &block)
@name = name
#...
instance_eval(&block) if block_given?
#...
end
# Rest as before...
end

This version uses instance_eval to evaluate the block in the context of the Project instance being configured. Any implicit references to self within the block are referring to that instance.

This makes the configuration block cleaner and more readable, especially in the context of a DSL. But there are (at least) two potential issues with it:

  1. The configuration block has access to any method, public or private, defined on Project. In this case I’m OK with that, but it might be an issue in other circumstances. If I was worried about it, I would introduce a separate configuration object that has only the desired API and use instance_eval on that object instead.

  2. The configuration block cannot use any methods local to the calling context. For example:

Trying to Use a Local Method
project "myProject" do
# Doesn't work!!
link_with libraries_to_link_with
end
def libraries_to_link_with
# Figure out which libraries to link with
end

This doesn’t work, because the block is evaluated such that self is the Project instance, and so can’t see the local libraries_to_link_with method. In this case, the client really needs to use the original block syntax with the explicit argument:

Using a Local Method
project "myProject" do |p|
p.link_with libraries_to_link_with
end
def libraries_to_link_with
# Figure out which libraries to link with
end

Since we changed our implementation, though, this option is no longer available. The solution is to make the configuration block handling code more flexible:

Flexible Configuration Block
class Project
def initialize(name, &block)
@name = name
if block_given?
if block.arity == 1
yield self
else
instance_eval(&block)
end
end
end
# Rest as before...
end

Now, we look at the block to see if it needs an argument or not. If it does, we use the original yield self approach; otherwise, we use the instance_eval approach.

This gives the client code the option of which form to use. In the normal case, no block argument is required and the client can use the cleaner form of configuration block. However, if the client needs to do something more complex, the original form is still available as well.

I learned about both the instance_eval approach and the flexible approach from this excellent post by Michael Bleigh.