Skip to content

07 · Metaprogramming Basics

Metaprogramming is writing code that writes (or modifies) code at runtime. Ruby is exceptionally good at this — classes stay open, methods can be defined dynamically, and objects can be inspected and reshaped while the program is running. This is how gems like ActiveRecord generate accessor methods for database columns nobody hand-wrote. Used well, it eliminates repetitive boilerplate; used carelessly, it produces code that's nearly impossible to trace. This lesson covers the three core tools: define_method, method_missing, and respond_to_missing?.

define_method — defining methods programmatically

define_method defines a real, ordinary instance method, but lets you compute its name and body in a loop instead of writing def by hand for each one:

class Product
  ATTRIBUTES = [:name, :price, :sku]

  def initialize(data)
    @data = data
  end

  ATTRIBUTES.each do |attribute|
    define_method(attribute) do
      @data[attribute]
    end
  end
end

product = Product.new(name: "Widget", price: 9.99, sku: "W-100")
puts product.name    # Widget
puts product.price    # 9.99

This is exactly what attr_reader does internally — it's define_method under the hood. Writing your own version like this is useful whenever the attribute list itself is dynamic (loaded from a config file or a database schema) rather than known when you write the class.

define_method vs method_missing — prefer define_method

Both can create "virtual" methods, but they behave very differently:

# method_missing: intercepts every unknown call, every time, forever
class LazyRecord
  def initialize(data)
    @data = data
  end

  def method_missing(name, *)
    @data.key?(name) ? @data[name] : super
  end

  def respond_to_missing?(name, include_private = false)
    @data.key?(name) || super
  end
end

# define_method: pays the cost once, up front, then behaves like a real method
class EagerRecord
  def initialize(data)
    @data = data
    data.each_key { |key| self.class.define_method(key) { @data[key] } }
  end
end

respond_to?, .methods, and tools like method(:name) all work correctly and automatically with define_method — because it creates a genuine method. With method_missing, you must maintain respond_to_missing? by hand to keep those tools honest (see OOP Deep Dive for the full footgun list). Rule of thumb: reach for define_method whenever you know the possible method names ahead of time; reserve method_missing for when the names are genuinely open-ended (like a schemaless data wrapper that must accept any key).

instance_variable_get / instance_variable_set

Ruby lets you read and write instance variables by name, even ones the class didn't explicitly expose:

class Point
  def initialize(x, y)
    @x = x
    @y = y
  end
end

point = Point.new(3, 4)
puts point.instance_variable_get(:@x)   # 3
point.instance_variable_set(:@x, 10)
puts point.instance_variable_get(:@x)   # 10
puts point.instance_variables.inspect     # [:@x, :@y]

This is how generic serialization libraries (converting any object to a hash) work without each class needing to implement a to_h method itself — though bypassing a class's own accessors like this skips any validation those accessors might do, so use it sparingly, mostly for tooling.

send — calling a method by name (including private ones)

class Account
  def initialize(balance)
    @balance = balance
  end

  private

  def apply_interest(rate)
    @balance *= (1 + rate)
  end
end

account = Account.new(100)
account.send(:apply_interest, 0.05)   # bypasses the `private` keyword
puts account.instance_variable_get(:@balance)   # 105.0

send calls a method by its symbol/string name and can call private methods, which makes it powerful for tooling and testing but dangerous in application code — it silently breaks encapsulation. Prefer public_send when the method name comes from outside your program (user input, config), since it respects private/protected and raises NoMethodError instead of quietly executing code that was meant to be internal-only.

define_method with metaprogrammed method names

A common pattern: generating both a getter and a "predicate" (?) method from the same data in one pass:

class FeatureFlags
  def initialize(flags)
    flags.each do |name, enabled|
      define_singleton_method("#{name}?") { enabled }
    end
  end
end

flags = FeatureFlags.new(dark_mode: true, beta_ui: false)
puts flags.dark_mode?   # true
puts flags.beta_ui?       # false

define_singleton_method defines the method on this one object only, not on the whole class — useful when different instances legitimately need different dynamic methods.

class_eval and instance_eval — evaluating code in another context

class_eval runs a block as if it were written inside the class body, letting you reopen and modify a class from outside its definition:

String.class_eval do
  def blank?
    strip.empty?
  end
end

puts "   ".blank?   # true
puts "hi".blank?     # false

This is (again) monkey-patching — see the risks discussed in OOP Deep Dive. class_eval is how gems like Rails add methods to core classes; it's the same mechanism as reopening the class with class String ... end, just usable when you only have a reference to the class object, not its literal name.

The core metaprogramming footgun: debuggability

Every technique on this page trades a little bit of stack-trace clarity for less boilerplate. A NoMethodError on a define_method-generated method points at the define_method call, not a named def line; a bug inside method_missing can be genuinely confusing to track down because the method "doesn't exist" anywhere you can grep for it. Use these tools when the boilerplate they eliminate is large and repetitive — not as a default way to write every class.

How It Actually Works

define_method, send, and class_eval all operate on the exact same method table that ordinary def populates — there is no separate "dynamic method" storage in MRI. class_eval works by temporarily changing what self and the default definee (where a bare def would land) refer to inside the block, evaluating your code as if it were written literally inside class Foo; end — which is why attr_accessor and def both work normally inside a class_eval block. send bypasses Ruby's usual private/protected visibility checks entirely because those checks happen at the call site during method dispatch, not inside the method lookup itself — send performs the identical ancestor-chain lookup as a normal call but skips the visibility gate, which is exactly why it's both useful for testing private methods and dangerous in production code. respond_to? and instance_variable_get/set are introspection into the literal same data structures method dispatch uses: the method table for the former, and the per-object instance-variable slot table for the latter — there's no reflection layer sitting on top, you're reading MRI's own bookkeeping directly.

Cheat sheet

Tool What it does
define_method(name) { ... } defines a real method dynamically
method_missing intercepts calls to undefined methods
respond_to_missing? keeps respond_to? honest alongside method_missing
instance_variable_get/set read/write an ivar by name
send / public_send call a method by name; send bypasses private
define_singleton_method defines a method on one specific object
class_eval runs a block as if inside the class body (reopens it)

Exercise

Write a class Struct2 (don't worry about the name clash with Ruby's real Struct) whose initialize(attributes) takes a hash and uses define_method to create a getter for every key, plus a to_h method that returns @attributes unchanged. Then add a class method Struct2.with_validations(*required_fields) that raises ArgumentError in initialize if any required field is missing from the hash.