Ruby DSL with instance_eval
A domain-specific language lets a block describe something in words that fit one job. This lesson builds one with a small builder class and instance_eval.
A block of bare calls
The block calls title and item with no receiver in front of them. instance_eval inside build makes the builder the receiver for those calls.
class ListBuilder
def initialize
@heading = ""
@items = []
end
def title(text)
@heading = text
end
def item(name)
@items << name
end
def build(&block)
instance_eval(&block)
self
end
def show
puts @heading
@items.each { |name| puts "- #{name}" }
end
end
list = ListBuilder.new
list.build do
title "Groceries"
item "Milk"
item "Eggs"
end
list.showThe builder collects in order
Each item call adds one entry to the stored list. show prints the entries in the order the block named them.
class ListBuilder
def initialize
@heading = ""
@items = []
end
def title(text)
@heading = text
end
def item(name)
@items << name
end
def build(&block)
instance_eval(&block)
self
end
def show
puts @heading
@items.each { |name| puts "- #{name}" }
end
end
list = ListBuilder.new
list.build do
title "Packing"
item "Rope"
item "Lamp"
item "Map"
end
list.showReusing the builder for a second list
A fresh builder builds a second description with the same words. Each builder keeps its own heading and items.
class ListBuilder
def initialize
@heading = ""
@items = []
end
def title(text)
@heading = text
end
def item(name)
@items << name
end
def build(&block)
instance_eval(&block)
self
end
def show
puts @heading
@items.each { |name| puts "- #{name}" }
end
end
first = ListBuilder.new
first.build do
title "Groceries"
item "Milk"
end
second = ListBuilder.new
second.build do
title "Tools"
item "Hammer"
end
first.show
second.showThe stored list prints again
The builder stores the description instead of printing it during the block. Calling show a second time prints the same stored list again.
class ListBuilder
def initialize
@heading = ""
@items = []
end
def title(text)
@heading = text
end
def item(name)
@items << name
end
def build(&block)
instance_eval(&block)
self
end
def show
puts @heading
@items.each { |name| puts "- #{name}" }
end
end
list = ListBuilder.new
list.build do
title "Groceries"
item "Milk"
end
list.show
list.showExample
class ListBuilder
def initialize
@heading = ""
@items = []
end
def title(text)
@heading = text
end
def item(name)
@items << name
end
def build(&block)
instance_eval(&block)
self
end
def show
puts @heading
@items.each { |name| puts "- #{name}" }
end
end
list = ListBuilder.new
list.build do
title "Reading"
item "Emma"
item "Dune"
item "Hamlet"
end
list.showKey points
- A builder class holds plain methods such as title and item.
- instance_eval runs a block with the builder as self.
- Bare calls inside the block land on the builder.
- The builder stores each call in its own state.
- The same builder class can build many descriptions.
In this example, a ListBuilder is driven by one block written as bare title and item calls. instance_eval makes the builder receive those calls, so it stores the heading and the three items in order. show then prints the stored description from the top.
Note
The block reads like a list, while ordinary method calls do the collecting.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
Why do the bare calls inside the block reach the builder?