Tons of people in the Ruby community go on and on about domain-specific languages (abbreviated DSL) and how wonderful they are. In most cases, I agree with them. I began to wonder how I could go about leveraging Ruby’s awesomely-flexible syntax to create my own DSL. To illustrate my quest, I have written this article. It assumes you know Ruby. The example details are completely fictitious and just contrived enough to be interesting. I promise, the code will be the main focus and I will link/point to other resources which might be helpful to budding DSL designers like myself. Problem The Dream Castle Architectural Firm (D-CAF, among friends) wants a tool which it can use for very high-level prototyping of custom homes. They need it to be understandable by both computers and humans. It only needs to keep track of the following things: - Houses - Each house has a name - Floors - Each house has multiple floors. Each floor has a
number
- Rooms - Each floor has multiple rooms and each room has a type
The only other requirement is that the DSL be translatable to plain English so that they can show it to customers. No problem. Solution The DSL for the high-level specification of custom houses will look like the following:
CustomHouse.build :home do
floor(1) {
room :den
room :kitchen
}
floor(2) {
room :bedroom
room :bathroom
}
end
That’s it. You specify that a house should be built, that it has a
name, some floors and each floor has some rooms. Simple, easy and
100% pure Ruby. Notice the cunning usage of both do/end and bracket
notation for defining blocks. The outer block passed to
CustomHouse#build uses do/end syntax while the blocks passed to
House#floor use the bracket syntax. These could easily be
reversed (or combined or whatever) but it makes it look pretty and
helps to visually differentiate things so that you (the developer)
and the user (the architect) can things more clearly. Plus, when
you print the output (calling to_s on the instance of House
which gets returned), you get the following wonderful text:
House named home has 2 floors.
Floor 1 has 2 rooms (den, kitchen)
Floor 2 has 2 rooms (bedroom, bathroom)
It’s a small house, don’t be a wiseacre. So, how can such a DSL be
built? Implementation Let’s write the implementation for this
together. First, we’re going to start of with a module named
CustomHouse. In Ruby, modules are just classes so we’ll define a
class method called build which will behave like a factory
method.
module CustomHouse
def self.build(name, &block)
house = House.new(name)
house.instance_eval(&block)
return house
end
end
As you can see, the method takes two params, a name and a block.
The first thing that it does is create an instance of the House
class (which we have not yet defined) passing in the name
parameter. Second it
calls instance_eval
on the house passing in the block. This ventures into the territory
of metaprogramming
which is great fun but beyond the scope of this document, though I
am sure others have used it for DSL construction (coincidentally,
if you are interested in Ruby metaprogramming, buy
this book). Suffice it to say that it
executes the supplied block in the context of the House instance.
Finally, the CustomHouse#build method returns the instance
of House. (There are those who believe that *any* type of eval
is fundamentally evil. I have been taught this many times and I try
to avoid using eval on an actualy string whenever possible. Still,
someone with a better understanding of Ruby internals might be able
to better explain if this in any better.) Next, let’s define what
the code for the House class looks like. As a note, it will be
defined in the CustomHouse module, technically making the
fully-namespaced name of the class CustomHouse::House.
class House
attr_accessor :name, :floors
def initialize(name = '')
@name = name.to_s
@floors = []
end
def floor(number, &block)
fl = Floor.new(number)
fl.instance_eval(&block)
@floors < < fl
end
def to_s
str = "House named #{@name} has #{@floors.length} floors.\n"
@floors.each do |f|
str << f.to_s
end
str
end
end
The above code should appear relatively simple. The House class has
only a few methods. First, it has a constructor function which
takes a name and a block, setting the name as an instance variable
and another instance variable, @floors, which is just an empty
array. Next, it has a method called floor which takes a number
and a block. The guts of this method should look familiar to you
because it mimics almost exactly the build factory method defined
on CustomHouse. Finally, it has a to_s method because of the
requirement that the DSL be translatable to plain English for
clients to check out. If we can just dwell for a moment on the
floor method, notice that, it too, takes a block and uses
instance_eval. It then adds the newly-constructed instance of
Floor to the array in @floor. Let’s look at the Floor class
now.
class Floor
attr_accessor :number, :rooms
def initialize(number = 0)
@number = number
@rooms = []
end
def room(type)
@rooms < < Room.new(type)
end
def to_s
str = "Floor #{@number} has #{@rooms.length} rooms ("
@rooms.each do |r|
str += "#{r.type}, "
end
str.chop!.chop!
str += ")\n"
str
end
end
There shouldn’t be anything confusing about the above code as it
doesn’t use any sort of block eval. In fact, the only thing left to
look at is the class for Room which is even less impressive.
class Room
attr_reader :type
def initialize(type)
@type = type
end
end
That’s it. Seriously, that’s the entire implementation of the DSL.
You pass a block to CustomHouse#build which gets executed in the
context of a new instance of House. The block calls the
House#floor method with a block which in turn gets executed in
the context of a new instance of Floor. The Floor#room method
adds new Room instances to the class and that’s basically it.
Here’s all the code together with the example:
module CustomHouse
def self.build(name, &block)
house = House.new(name)
house.instance_eval(&block)
return house
end
class House
attr_accessor :name, :floors
def initialize(name = '')
@name = name.to_s
@floors = []
end
def floor(number, &block)
fl = Floor.new(number)
fl.instance_eval(&block)
@floors << fl
end
def to_s
str = "House named #{@name} has #{@floors.length} floors.\n"
@floors.each do |f|
str << f.to_s
end
str
end
end
class Floor
attr_accessor :number, :rooms
def initialize(number = 0)
@number = number
@rooms = []
end
def room(type)
@rooms << Room.new(type)
end
def to_s
str = "Floor #{@number} has #{@rooms.length} rooms ("
@rooms.each do |r|
str += "#{r.type}, "
end
str.chop!.chop!
str += ")\n"
str
end
end
class Room
attr_reader :type
def initialize(type)
@type = type
end
end
end
h = CustomHouse.build :home do
floor(1) {
room :den
room :kitchen
}
floor(2) {
room :bedroom
room :bathroom
}
end
puts h
Try running it and see what happens! Then try writing other definitions for custom houses and experience the theoretical joy of the hypothetical architectural firm. DSL construction techniques For clarification and context, I’d like to share some other, smaller examples which build on this technique and demonstrate one more. Behold, a DSL for feeding Pandas:
Panda.feed {
nom :bamboo
nom :chocolate
}
The implementation of this is both simple and straightforward.
class Panda
def self.feed(&block)
panda = Panda.new
panda.instance_eval(&block)
end
def nom(food)
#whatever
end
end
Since the block is evaluated in the context of the new Panda
instance, it has access to the Panda#nom method. For people
deathly afraid of eval, there is this alternative syntax:
Panda.feed do |p|
p.nom :bamboo
p.nom :chocolate
end
Which is implemented with yield instead of instance_eval like
so:
class Panda
def self.feed
yield Panda.new
end
def nom(food)
# whatever
end
end
For a wonderful and inspiring treatment of Ruby DSLs and associated patterns, see the most-excellent talk on the matter given by Jeremy McAnally at the 2009 Mid West Ruby Conference. Conclusion DSLs are a fantastic tool which can help to simplify complicated and repetitive tasks. Ruby is very good for creating DSLs but it is not the only good tool out there. I advise you look into the creation of DSLs with Scala and, the best DSL-creation tool there ever was, Lisp Macros. I am interested in improving this tutorial for the benefit of those programmers who wish to learn about DSL construction but don’t know where to start.