Advanced Ruby: Metaprogramming and DSLs [3d]

Rubymonk training

Read the following Rubymonk articles:

For each chapter in each article:

  • Play with the introduced Ruby feature using a simple Ruby script or IRB console
  • Talk with your mentor what you learned.

Pros and Cons of DSLs

The ability to write your own domain specific language (DSL) can be a very powerful tool. Most notably, it allows you to represent data in a very readable manner. For example, Rails offers a compact DSL Show archive.org snapshot to define new routes.
A big drawback however is that your code often becomes much harder to read. Another con of DSLs is that they tend to be so specific that its users are prevented from using local variables, methods, modules and many other Ruby features. Working around these issues can be quite hacky.

More resources

Exercise: Roll your own DSL

This entire exercise should be implemented using pure Ruby, without any gems.

Write an Addressbook class that can be used like this:

book = Addressbook.new

book.add_contact 'Henning Koch'
book.add_contact 'Tobias Kraze'

book.contacts # => ['Henning Koch', 'Tobias Kraze']

Now change Addressbook so contacts can be defined with a custom DSL:

book = Addressbook.parse do
  contact 'Henning Koch'
  contact 'Tobias Kraze'
end

book.contacts # => ['Henning Koch', 'Tobias Kraze']

Tip

You can use instance_exec to run a block on another self.

Now allow each contact to have a phone and email attribute:

book = Addressbook.parse do

  contact 'Henning Koch' do
    phone '12345'
    email 'foo@bar.de'
  end

  contact 'Tobias Kraze' do
    phone '67890'
    email 'bam@baz.de'
  end

end

book.find('Henning Koch') # => { :phone => '12345', :email => 'foo@bar.de' }
book.find('Tobias Kraze') # => { :phone => '67890', :email => 'bam@baz.de' }

Now change Addressbook so contacts can be accessed by their underscored names:

book.henning_koch # => { :phone => '12345', :email => 'foo@bar.de' }
book.tobias_kraze # => { :phone => '67890', :email => 'bam@baz.de' }

Now change Addressbook so each contact becomes their own Contact instance which responds to #phone and #email:

book.henning_koch # => Contact<#....>
book.henning_koch.phone # => '12345'
book.henning_koch.email # => 'foo@bar.de'

Now allow arbitrary fields, not just phone and email:

book = Addressbook.parse do

  contact 'Henning Koch' do
    phone '12345'
    glasses true
    shirt 'red'
  end

end

book.henning_koch.shirt # => 'red'

Exercise: DSL styles

The DSL above could also be implemented using this syntax:

book = Addressbook.parse do |ab|

  ab.contact 'Henning Koch' do |c|
    c.phone '12345'
    c.glasses true
    c.shirt 'red'
  end

end

book.henning_koch.shirt # => 'red'

Change your implementation to work like this.

What are the advantages of this style of DSL? What are the drawbacks? Which do you prefer?

You have probably encountered examples of both styles before. Name a few.

Excercise: Modularity

Consider the following example from the Modularity Show archive.org snapshot README:

# app/models/article.rb
class Article < ActiveRecord::Base
  include DoesStripFields[:name, :brand]
end

# app/models/shared/does_strip_fields.rb
module DoesStripFields
  as_trait do |*fields|
    fields.each do |field|
      define_method("#{field}=") do |value|
        self[field] = value.strip
      end
    end
  end
end

Go through the Modularity source code and understand how the implementation works. In particular, understand this syntax:

include DoesStripFields[:name, :brand]

What exactly is included here? How does Modularity enable parameterized modules? How do the square brackets work?

Henning Koch Over 8 years ago