Metaprogramming Basics

Ruby Intermediate/Advanced — Metaprogramming Basics
Ruby Intermediate/Advanced
Course 2 · Chapter 2 · Metaprogramming Basics

🪄 Metaprogramming Basics

This chapter is where Ruby's reputation for "code that writes code" actually comes from. Four tools — 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)().

⚠ send can call private methods too

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
⚠ Always pair method_missing with respond_to_missing?

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

ConceptPHPPythonRuby
Call method by name$obj->{$name}()getattr(obj, name)()obj.send(name)
Check method existsmethod_exists()hasattr()respond_to?
Handle undefined method calls__call() / __callStatic()__getattr__()method_missing
Generate methods dynamicallyNo 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.

→ Solution

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.

→ Solution

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.

→ Solution

💎 Just Because You Can Doesn't Mean You Should

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.