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_methodis the safest place to start when you want to generate methods dynamically.method_missingis useful when method names are not known ahead of time.sendis powerful, but it should be used carefully because it can bypass method visibility.evalcan 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 dynamicallymethod_missing— Handle calls to undefined methodssend— Invoke methods by name at runtimeeval— 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
evalbefore tryingdefine_methodor 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_missingswallow 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_methodcreates methods programmaticallymethod_missingintercepts calls to undefined methodssendinvokes methods by name at runtimeevalexecutes 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
- Ruby Open Classes and Monkey Patching — the broader context for modifying Ruby classes at runtime
- method_missing and respond_to_missing? — dynamic method handling in more detail