๐ŸŽ“ A1Learner
๐Ÿ”
0 of 28 lessons completed

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.show

The 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.show

Reusing 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.show

The 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.show

Example

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.show

Key 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?