rubyguides

Metaprogramming basics in Ruby

Metaprogramming is one of Ruby’s most powerful features, allowing you to write code that writes code. This guide to metaprogramming basics starts with the tools you will reach for most often: define_method, method_missing, send, and eval, and shows how each one solves a specific category of dynamic programming problem. Instead of explicitly defining every method, class, or module, metaprogramming lets you create these dynamically at runtime. The techniques covered here form the backbone of advanced Ruby programming.

Key takeaways

  • define_method is the safest place to start when you want to generate methods dynamically.
  • method_missing is useful when method names are not known ahead of time.
  • send is powerful, but it should be used carefully because it can bypass method visibility.
  • eval can generate code at runtime, but it deserves extra scrutiny because it is harder to debug and easier to misuse.
  • The best metaprogramming is targeted, readable, and easy to explain to another developer.

If you remember one rule, make it this: use metaprogramming to reduce repetition or express a clear DSL, not to hide ordinary Ruby behind cleverness. The reader should still be able to trace what the code does without reverse-engineering the whole class.

What is Metaprogramming?

Metaprogramming refers to techniques that treat programs as data: code that can be inspected, modified, and generated while the program runs. Ruby provides several built-in mechanisms for this:

  • define_method — Create methods dynamically
  • method_missing — Handle calls to undefined methods
  • send — Invoke methods by name at runtime
  • eval — Execute strings as code

These tools are used extensively in popular gems like Rails, RSpec, and ActiveRecord.

The common thread is that Ruby gives you a lot of control over how methods appear, how they are called, and when they are defined. That freedom is part of what makes the language so expressive, but it also means the code can become opaque if you do not keep the dynamic parts small and well named.

When is metaprogramming worth it?

Metaprogramming is worth it when the repetition is real and the pattern is stable. If you are generating the same family of methods, translating method names into lookups, or building a small internal DSL, the abstraction can make the code shorter and clearer. If the logic is only used once, the dynamic layer often adds more noise than value.

That is why many Ruby libraries use metaprogramming sparingly. They generate just enough API to keep the call site pleasant, but they still leave the implementation understandable enough for maintainers to reason about later.

Dynamic methods with define_method

The define_method lets you create methods programmatically. Instead of writing each method explicitly, you can generate them based on data or configuration.

class Calculator
  %w[add subtract multiply divide].each do |operation|
    define_method(operation) do |a, b|
      case operation
      when 'add'      then a + b
      when 'subtract' then a - b
      when 'multiply' then a * b
      when 'divide'   then b.zero? ? nil : a / b
      end
    end
  end
end

calc = Calculator.new
calc.add(10, 5)        # => 15
calc.multiply(3, 4)    # => 12

This pattern is common in Rails, where methods like has_many and validates generate dozens of methods based on configuration.

You can think of define_method as the most readable metaprogramming tool in the language. The code still looks like Ruby, but you are describing methods from data instead of writing each one by hand.

Handling missing methods with method_missing

When you call a method that doesn’t exist, Ruby normally raises a NoMethodError. But you can intercept these calls using method_missing. This is how Rails’ dynamic finders work: find_by_email doesn’t exist until you call it.

class DynamicFinder
  def method_missing(method_name, *args, &block)
    if method_name.to_s.start_with?("find_by_")
      attribute = method_name.to_s.sub("find_by_", "")
      find_by_attribute(attribute, args.first)
    else
      super
    end
  end

  def respond_to_missing?(method_name, include_private = false)
    method_name.to_s.start_with?("find_by_") || super
  end

  private

  def find_by_attribute(attr, value)
    puts "Searching by #{attr} for #{value}"
    # Simulated database lookup
    "Found record #{attr}: #{value}"
  end
end

finder = DynamicFinder.new
finder.find_by_email("user@example.com")
finder.find_by_username("johndoe")

The respond_to_missing? method ensures the dynamic methods are reported correctly when something checks what methods an object responds to.

That pairing is important because it keeps reflection honest. Code that asks an object what it supports should get the same answer that a direct call would imply.

Sending messages with send

The send method lets you call any method by its name as a string or symbol. This is useful for invoking methods programmatically, especially when building frameworks or testing tools.

class Speaker
  def hello
    puts "Hello!"
  end

  def goodbye
    puts "Goodbye!"
  end

  def greet(name)
    puts "Hello, #{name}!"
  end
end

speaker = Speaker.new
speaker.send(:hello)              # => Hello!
speaker.send("goodbye")            # => Goodbye!
speaker.send(:greet, "Alice")     # => Hello, Alice!

# Useful for calling methods from configuration
actions = [:hello, :goodbye, :greet]
actions.each { |action| speaker.send(action) }

send bypasses privacy checks. Use public_send if you need to respect visibility.

That distinction matters in libraries and frameworks. send can be convenient in a trusted environment, but public_send is the safer option when the receiver should only expose its public API.

Evaluating code with eval

For complete runtime code generation, Ruby’s eval executes a string as Ruby code. Use it sparingly; it is powerful but can be dangerous and hard to debug.

# Simple eval example
result = eval("10 + 5")
puts result  # => 15

# Dynamic method definition
class Example
  eval <<-RUBY
    def dynamic_method
      "I was created at runtime!"
    end
  RUBY
end

e = Example.new
puts e.dynamic_method  # => I was created at runtime!

For safer alternatives, consider instance_eval, class_eval, and module_eval (also available as module_exec and class_exec).

Those alternatives are still powerful, but they usually keep the code a little closer to Ruby objects and a little further from raw strings. That makes them easier to inspect later and reduces the temptation to build one giant string of code.

When to use metaprogramming

Metaprogramming shines when building:

  • Domain-specific languages (DSLs)
  • Frameworks that generate code based on configuration
  • APIs that provide convenience methods dynamically
  • Testing libraries with expressive syntax

Avoid metaprogramming when simple, explicit code would work. The flexibility comes at the cost of readability and debugging difficulty.

Common mistakes

  • Reaching for eval before trying define_method or a plain helper method.
  • Hiding simple business logic behind dynamic dispatch that nobody expects.
  • Forgetting to test the actual public API that the dynamic methods expose.
  • Letting method_missing swallow errors that should have been obvious.

Frequently asked questions

Is metaprogramming a sign of advanced Ruby code?

It can be, but not every use of metaprogramming is advanced or beneficial. The goal is to make the code easier to work with, not to make it look clever.

Should I prefer define_method over method_missing?

Usually yes. define_method is easier to reason about because the method actually exists after it is generated. Use method_missing when the set of method names is open-ended or depends on the input.

Is eval always a bad idea?

Not always, but it should be the last option rather than the first. If the same problem can be solved with define_method, a closure, or a normal helper, those options are usually safer and easier to maintain.

When not to use metaprogramming

Don’t use metaprogramming for:

  • Simple cases that can be solved with plain methods
  • Code that others will maintain without understanding the dynamic behavior
  • Performance-critical sections where method lookup overhead matters
  • Security-sensitive code where eval could introduce vulnerabilities

If you find yourself writing complex metaprogramming to solve a simple problem, step back and consider a more straightforward approach.

Summary

Ruby’s metaprogramming capabilities give you extraordinary flexibility:

  • define_method creates methods programmatically
  • method_missing intercepts calls to undefined methods
  • send invokes methods by name at runtime
  • eval executes arbitrary code strings

Use these tools judiciously. The best metaprogramming is invisible: users should not need to understand the dynamic underpinnings to use your code effectively.

Conclusion

Ruby metaprogramming is most valuable when it reduces duplication or lets you describe a pattern once and reuse it many times. define_method, method_missing, send, and eval are all part of that toolbox, but they solve different kinds of problems. The simplest one that fits the job is usually the best one.

If you keep the public API obvious and the dynamic layer narrow, you get the flexibility without turning the class into a puzzle. That balance is what makes Ruby metaprogramming feel powerful instead of fragile.

See Also