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
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
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
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:
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.