315 Advanced Ruby: Metaprogramming and DSLs [3d]

Updated . Posted . Visible to the public.

Ruby lets programs define methods and classes at runtime. This lesson covers metaprogramming techniques and how to use them to build a domain-specific language (DSL), along with the price you pay in readability.

Important

Work on this lesson in builder mode.

Learning goals

  • You can explain what metaprogramming means in Ruby: code that defines or changes classes and methods while the program runs.
  • You can define methods dynamically (e.g. with define_method) and handle calls to methods that don't exist (method_missing together with respond_to_missing?).
  • You can run a block in the context of another object (e.g. with instance_exec) and explain why DSLs depend on this.
  • You can explain what a domain-specific language is, and weigh its benefit — data that reads like prose — against its costs: code that is harder to read, and users locked into the DSL's vocabulary.
  • You can design and implement a small DSL in plain Ruby, in both the implicit-receiver style and the explicit-receiver style, and argue when to use which.
  • You can read metaprogramming code in a library and explain how it works, e.g. how Modularity implements parameterized modules.
  • You use metaprogramming sparingly, because it hides definitions from readers and tools and produces confusing errors.

Resources

Read what's new to you, skim what's familiar, skip what you already master. Stop when you can meet the learning goals.

Your agent can also generate an overview, a tutorial or an explanation for anything here, tailored to what you already know. Just ask.

Exercises

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'

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.

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?

Profile picture of Henning Koch
Henning Koch
Last edit
Henning Koch
License
Source code in this card is licensed under the MIT License.
Posted by Henning Koch to makandra Curriculum (2015-08-05 13:12)