04 · Metaprogramming Advanced¶
Level 1/2 metaprogramming touched method_missing and send in passing.
This module goes deeper into the tools Ruby gives you to write code that
writes code: reopening classes at runtime (class_eval), running a block
in the context of a specific object (instance_eval), and generating
methods programmatically (define_method). These are the mechanisms
behind DSLs like RSpec's describe/it and Rails' has_many.
class_eval — reopening a class dynamically¶
Ruby classes are never "closed" — you can reopen Widget and add methods
to it at any point, including from a variable holding the class itself
(useful when the class name is computed, not a literal):
class Widget
end
Widget.class_eval do
def greet
"Hi from #{self.class}"
end
end
puts Widget.new.greet
class_eval runs the block as if it were written directly inside the
class body — def inside it defines an instance method exactly like a
normal class Widget ... end block would. This is how gems patch or
extend classes they don't own (monkey-patching), and how ORMs like
ActiveRecord inject association methods into your model classes at load
time.
method_missing — intercepting unknown calls¶
Every object has method_missing, called whenever a method lookup
fails. Overriding it lets an object respond to method names it never
explicitly defined:
class Config
def initialize
@settings = {}
end
def method_missing(name, *args)
name_str = name.to_s
if name_str.end_with?("=")
@settings[name_str.chomp("=").to_sym] = args.first
elsif @settings.key?(name)
@settings[name]
else
super
end
end
def respond_to_missing?(name, include_private = false)
true
end
end
c = Config.new
c.timeout = 30
puts c.timeout
c.timeout = 30 doesn't call a real timeout= method — Ruby fails the
lookup, then calls method_missing(:timeout=, 30), which stores the
value in @settings. Reading c.timeout triggers the same path in
reverse.
define_method — generating methods from data¶
method_missing intercepts every failed call at runtime, which is
flexible but slower and harder to introspect. When you know the method
names ahead of time (even if only as a list computed at class-definition
time), define_method is the better tool — it creates a real, normal
method:
class Person
[:name, :age].each do |attr|
define_method(attr) { instance_variable_get("@#{attr}") }
define_method("#{attr}=") { |val| instance_variable_set("@#{attr}", val) }
end
end
p = Person.new
p.name = "Ada"
p.age = 36
puts "#{p.name}, #{p.age}"
This is exactly what attr_accessor does under the hood, and it's the
pattern used to generate the association methods (has_many :books
producing a real books method) that class_eval sets up on your
behalf in a real ORM.
instance_eval — running a block against one object¶
instance_eval changes self for the duration of a block to be the
receiver, giving the block direct access to that object's instance
variables and private methods — without needing accessor methods:
This is the mechanism behind builder-style DSLs:
class HTMLBuilder
def initialize(&block)
@output = +""
instance_eval(&block)
end
def tag(name, text)
@output << "<#{name}>#{text}</#{name}>"
end
def to_s = @output
end
page = HTMLBuilder.new do
tag "h1", "Title"
tag "p", "Body text"
end
puts page.to_s
# <h1>Title</h1><p>Body text</p>
Inside the block, tag resolves against HTMLBuilder's instance
(because self was reassigned there), not against wherever the block was
originally written — which is exactly what lets you write tag "h1",
"Title" instead of builder.tag("h1", "Title").
class_eval vs instance_eval vs define_method — which one¶
class_eval: add/redefine instance methods on a class you have a reference to, especially one whose name you don't know until runtime.instance_eval: run a block withselfpointed at one specific object — the core trick behind fluent/builder DSLs.define_method: generate methods programmatically from a known list of names, as a faster, more introspectable alternative tomethod_missing.
Metaprogramming-specific traps¶
- Forgetting
respond_to_missing?alongsidemethod_missingbreaksrespond_to?,.methods, and duck-typing checks — code that asks "does this object handlefoo?" gets a wrongfalseeven thoughmethod_missingwould happily handle it. method_missingwithout eventually callingsuperfor truly unhandled names swallows realNoMethodErrors, turning typos into silentnils or confusing custom errors instead of Ruby's normal, greppable exception.define_methodblock scope captures the enclosing scope (a closure) — this is a feature for the accessor pattern above, but it means a loop variable reused across iterations can leak into every generated method if you're not careful about what the block closes over.class_evalon a class you don't own is monkey-patching — it can silently override a method the gem author relies on internally, breaking in a way that's very hard to trace back to "line 40 of my initializer." Prefer refinements or composition when you don't have to reach into someone else's class.instance_evalchangesselfbut not local-variable scope — a block passed toinstance_evalstill closes over locals from where it was written, sotagmethods resolve against the newself, but a plain local variable referenced inside still means whatever it meant outside.
How It Actually Works¶
method_missing is the last stop in MRI's method lookup, not a special
case bolted on top of it: when the ancestor-chain walk (class, then
included modules, then superclass...) fails to find a matching method
anywhere, MRI's dispatch mechanism — instead of immediately raising
NoMethodError — calls method_missing on the receiver if one is defined,
passing the attempted method name and arguments; the default
BasicObject#method_missing simply raises the error you're used to seeing.
This is why overriding method_missing (and pairing it with
respond_to_missing?, which callers like respond_to? and method
consult) lets an object appear to respond to methods that were never
actually defined anywhere in its ancestor chain. define_method and
class_eval/instance_eval don't create some parallel "dynamic" method
type — they populate the exact same per-class method table that def
populates at parse time; the only difference is when the method table
gets the entry (compile time for def, arbitrary runtime for
define_method), which is why a define_method-defined method is
indistinguishable from a hand-written one once it exists.
Cheat sheet¶
| Task | Code |
|---|---|
| Reopen a class and add a method | SomeClass.class_eval { def m; end } |
Run a block with a different self |
obj.instance_eval { ... } |
| Intercept unknown method calls | def method_missing(name, *args); ...; end |
Keep respond_to? honest |
def respond_to_missing?(name, priv); ...; end |
| Generate a method from a name | define_method(:name) { ... } |
| Generate a method with args | define_method(:name) { \|x\| ... } |
| Call a private method externally | obj.send(:private_method) |
| Check defined methods | SomeClass.instance_methods(false) |
Exercise¶
Build a small AttributeBuilder module:
- A module
Attributablewith a class methodattribute(name, default: nil)that, whenextended into a class, usesdefine_methodto create a getter (returning the stored value ordefault) and setter for that attribute name. - A class
Taskthatextend Attributableand declaresattribute :titleandattribute :priority, default: "normal". - Demonstrate: creating a
Task.new, reading.prioritybefore setting it (should print"normal"), then setting and reading.title. - Add a
method_missing-based fallback onTaskthat raises a customNoSuchAttributeErrorfor any unknown attribute-style call, and show it triggering.
Run the script and confirm the printed values match what define_method
generated.