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
buildermode.
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_missingtogether withrespond_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.
- 📄
Ruby Metaprogramming: How to Write Dynamic Code
Show archive.org snapshot
— tutorial with runnable examples: reopening classes,
define_method,method_missing,instance_eval/class_eval, and building a small DSL (2025) - 📄 Ruby Metaprogramming: Method Missing Show archive.org snapshot — short and focused on the one technique most DSLs rely on
- 📄 Ruby docs:
Module#define_methodShow archive.org snapshot ,BasicObject#method_missingShow archive.org snapshot andBasicObject#instance_execShow archive.org snapshot — the reference for the three building blocks - 📄 When overriding
#method_missing, remember to override#respond_to_missing?as well — our card - 📘 Metaprogramming Ruby 2 by Paolo Perrotta — the book on the subject, if you want the long form (check our library)
- 📄 DSLs in the wild: factory_bot's partial factory definitions and
Rails'
Engineconfiguration Show archive.org snapshot — read how libraries you use every day are built on these techniques
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_execto run a block on anotherself.
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?