Metaprogramming Basics in Ruby
Metaprogramming is one of Ruby’s most powerful features, allowing you to write code that writes code. Instead of explicitly defining every method, class, or module, metaprogramming lets you create these dynamically at runtime. This guide covers the fundamental techniques that form the backbone of advanced Ruby programming.
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.
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.
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.
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.
Evaluating Code with eval
For complete runtime code generation, Ruby’s eval executes a string as Ruby code. Use it sparingly—it’s 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).
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.
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 shouldn’t need to understand the dynamic underpinnings to use your code effectively.