Metaprogramming Basics
๐ช Metaprogramming Basics
send, respond_to?, define_method, and method_missing โ let a program call methods by name at runtime, generate methods dynamically, and even respond to method calls that were never explicitly defined at all.
๐ send โ Calling a Method by Name
name = "ruby" name.send(:upcase) # => "RUBY" -- same as name.upcase name.send("upcase") # => "RUBY" -- a string works too method_name = :reverse name.send(method_name) # => "ybur" -- the whole point: the method name is a variable
send calls a method whose name is only known at runtime โ as a symbol or a string, computed however you like. This is directly comparable to PHP's variable method call syntax $obj->{$methodName}() or Python's getattr(obj, name)().
Unlike a normal method call, send bypasses Ruby's method visibility rules entirely โ it can invoke private methods from outside the class, which a regular obj.method call cannot do. This is occasionally genuinely useful (testing, introspection), but it's also an easy way to accidentally break encapsulation. Ruby does provide public_send, which respects visibility and raises NoMethodError on a private method exactly like a normal call would โ reach for that by default unless you specifically need to reach into private territory.
โ respond_to? โ Checking Before You Call
name = "ruby" if name.respond_to?(:upcase) puts name.upcase end 5.respond_to?(:upcase) # => false -- Integer has no upcase method
respond_to? checks whether an object has a given method โ the equivalent of PHP's method_exists() or Python's hasattr(). It's especially useful paired with send, when you're calling a dynamically-chosen method name and want to confirm it's actually valid first.
๐ญ define_method โ Generating Methods at Runtime
class Product [:name, :price, :sku].each do |attribute| define_method(attribute) do @data[attribute] end end def initialize(data) @data = data end end p = Product.new(name: "Widget", price: 9.99, sku: "W-001") p.name # => "Widget" p.price # => 9.99
define_method generates a real, ordinary instance method โ indistinguishable afterward from one written with def โ but from inside a loop, driven by data instead of hand-typed one at a time. This is, in effect, a more flexible version of Chapter 6's attr_accessor: attr_accessor is really just a convenience method that calls something conceptually similar to define_method internally.
๐ป method_missing โ Handling Calls to Undefined Methods
Every method call Ruby can't find triggers a special hook method called method_missing before raising NoMethodError. Override it, and you can intercept and handle calls to methods that were never explicitly defined at all:
class GhostResponder def method_missing(method_name, *args) if method_name.to_s.start_with?("ask_") "You asked about: #{method_name.to_s.sub('ask_', '')}" else super # important -- let genuinely unhandled calls raise normally end end def respond_to_missing?(method_name, include_private = false) method_name.to_s.start_with?("ask_") || super end end ghost = GhostResponder.new ghost.ask_weather # => "You asked about: weather" -- ask_weather was never defined! ghost.respond_to?(:ask_anything) # => true
Overriding method_missing alone makes an object answer to dynamic method calls, but respond_to? won't know about them unless you also override respond_to_missing? to match the same logic. Skipping this pairing is a very common bug: code that calls obj.respond_to?(:ask_weather) to check first will get false even though obj.ask_weather actually works โ a real, confusing inconsistency that's easy to introduce and easy to overlook in review.
PHP's closest equivalent is the __call/__callStatic magic methods; Python's is __getattr__. Ruby's method_missing plays the same conceptual role in each language's metaprogramming toolkit.
๐ Where This Actually Gets Used
This isn't just a party trick. Rails' ActiveRecord famously used method_missing-based "dynamic finders" (find_by_name, find_by_email โ one method handling infinitely many possible attribute names) for years before switching to define_method-based generation for better performance and clearer error messages. Ruby's standard library OpenStruct class โ an object you can assign arbitrary attributes to on the fly โ is built on exactly this mechanism.
๐ Metaprogramming Hooks, Side by Side
PHP vs Python vs Ruby
| Concept | PHP | Python | Ruby |
|---|---|---|---|
| Call method by name | $obj->{$name}() | getattr(obj, name)() | obj.send(name) |
| Check method exists | method_exists() | hasattr() | respond_to? |
| Handle undefined method calls | __call() / __callStatic() | __getattr__() | method_missing |
| Generate methods dynamically | No direct equivalent (eval-based workarounds) | setattr(cls, name, function) | define_method |
๐ป Coding Challenges
Challenge 1: Dynamic Dispatch With send
Given a Calculator class with methods add(a, b), subtract(a, b), and multiply(a, b), write a method calculate(operation, a, b) that uses send to call the correct method based on the operation symbol passed in. Use respond_to? to return nil gracefully for an unknown operation instead of raising.
Goal: Combine send and respond_to? the way they're commonly used together in practice.
Challenge 2: define_method in a Loop
Write a class Coordinate that uses define_method inside a loop over [:x, :y, :z] to generate three getter methods, backed by a single @values hash set in initialize. Confirm all three generated methods work on an instance.
Goal: Generate a family of near-identical methods from data instead of writing each one by hand.
Challenge 3: A Minimal OpenStruct
Write a class DynamicRecord whose initialize accepts a hash and stores it in @attributes. Override method_missing so any method call matching a key in @attributes returns that value, and override respond_to_missing? to match. Confirm both a dynamic call and respond_to? agree with each other.
Goal: Build a small version of what Ruby's real OpenStruct does, pairing method_missing with respond_to_missing? correctly.
Every technique in this chapter trades some amount of static clarity for dynamic flexibility. A method generated by define_method in a loop is genuinely harder to "find" with a simple text search than one written with def, and an object relying heavily on method_missing can be nearly impossible to understand from reading the class alone. Reach for these tools when the alternative is real, repetitive boilerplate (as in this chapter's examples) โ not by default, and not because it looks clever.
๐ฏ What's Next
Next chapter: Object Equality & Comparable โ ==, eql?, hash, the spaceship operator <=>, and mixing in the Comparable module to get <, >, and between? for free.