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.