Ruby’s metaprogramming capabilities stand as one of its most powerful features, enabling developers to write code that writes code. I’ve spent years exploring these techniques and implemented them in production systems with remarkable results. Let me share seven essential metaprogramming techniques that will transform how you think about Ruby development.
Method Definition at Runtime
Defining methods dynamically is perhaps the most common metaprogramming technique in Ruby. This approach allows us to generate methods based on runtime conditions or data.
class Product
def self.create_methods_for_properties(properties)
properties.each do |property|
define_method(property) do
instance_variable_get("@#{property}")
end
define_method("#{property}=") do |value|
instance_variable_set("@#{property}", value)
end
end
end
create_methods_for_properties [:name, :price, :category]
end
product = Product.new
product.name = "Ruby Programming Book"
puts product.name # "Ruby Programming Book"
This example creates getter and setter methods for each property in the array. The define_method
call dynamically creates instance methods on the class. Instead of manually writing accessors for each attribute, we generate them programmatically.
When working with larger data models, this approach significantly reduces code repetition. I once reduced a 500-line class with dozens of similar methods to under 100 lines using this technique.
Dynamic Class Creation
Creating classes at runtime offers tremendous flexibility for systems where the structure isn’t known until execution.
def generate_model_class(name, attributes)
klass = Class.new do
attributes.each do |attr|
attr_accessor attr
end
define_method(:initialize) do |**kwargs|
attributes.each do |attr|
instance_variable_set("@#{attr}", kwargs[attr])
end
end
define_method(:to_h) do
attributes.each_with_object({}) do |attr, hash|
hash[attr] = send(attr)
end
end
end
Object.const_set(name, klass)
end
# Generate a class based on API response
generate_model_class("User", [:id, :name, :email])
user = User.new(id: 1, name: "John", email: "[email protected]")
puts user.to_h # {:id=>1, :name=>"John", :email=>"[email protected]"}
This technique is especially useful when working with external APIs or dynamic database schemas. I’ve used it extensively in data processing pipelines where the structure of incoming data varies.
Method Delegation Patterns
Delegation is a powerful pattern that can be implemented elegantly with metaprogramming. Instead of manually forwarding method calls, we can use Ruby’s metaprogramming to create delegation patterns.
module Delegation
def delegate(*methods, to:)
methods.each do |method_name|
define_method(method_name) do |*args, &block|
receiver = send(to)
receiver.send(method_name, *args, &block)
end
end
end
end
class ShoppingCart
extend Delegation
delegate :size, :empty?, :each, to: :@items
def initialize
@items = []
end
def add_item(item)
@items << item
end
end
cart = ShoppingCart.new
cart.add_item("Book")
puts cart.size # 1
puts cart.empty? # false
cart.each { |item| puts item } # "Book"
This technique creates clean APIs and follows the principle of composition over inheritance. In a recent project, I used delegation to build a wrapper around a third-party API, which made integration much cleaner.
Extending Objects with Modules
Ruby allows us to extend individual objects with modules, giving them additional functionality at runtime.
module Admin
def admin_dashboard
"Admin dashboard for #{name}"
end
def can_delete_users?
true
end
end
class User
attr_accessor :name, :role
def initialize(name, role)
@name = name
@role = role
# Dynamically extend with capabilities based on role
extend Admin if role == :admin
end
end
regular_user = User.new("John", :user)
admin_user = User.new("Jane", :admin)
puts admin_user.admin_dashboard # "Admin dashboard for Jane"
puts admin_user.can_delete_users? # true
begin
regular_user.admin_dashboard # This will raise NoMethodError
rescue NoMethodError => e
puts "Regular users don't have admin capabilities"
end
This pattern allows for selective functionality based on context. I’ve used it extensively for role-based systems where different user types need different capabilities without creating complex inheritance hierarchies.
Code Evaluation Strategies
Ruby offers multiple ways to evaluate code at runtime, each with specific use cases and security implications.
# 1. Using eval (use with extreme caution)
def dangerous_eval(code)
eval(code)
end
# 2. Using instance_eval for object context
class Configuration
attr_accessor :host, :port, :username, :password
def initialize
@host = "localhost"
@port = 3000
end
def configure(&block)
instance_eval(&block)
end
end
config = Configuration.new
config.configure do
self.host = "production.example.com"
self.port = 443
self.username = "admin"
self.password = "secret"
end
puts "#{config.host}:#{config.port}" # "production.example.com:443"
# 3. Using class_eval for adding methods to a class
String.class_eval do
def palindrome?
self == self.reverse
end
end
puts "radar".palindrome? # true
puts "ruby".palindrome? # false
The instance_eval
approach is particularly useful for configuration DSLs. I created a testing framework that used this pattern to provide a clean syntax for test setup while maintaining access to the object context.
Domain Specific Language (DSL) Implementation
Ruby excels at creating internal DSLs that make code more expressive and domain-focused.
class HTMLBuilder
def initialize
@html = ""
end
def method_missing(name, *args, &block)
attributes = args.first.is_a?(Hash) ? args.shift : {}
content = args.first
# Opening tag with attributes
@html << "<#{name}"
attributes.each do |key, value|
@html << " #{key}=\"#{value}\""
end
if block_given? || content
@html << ">"
# Add content if provided
@html << content.to_s if content
# Execute block in builder context if given
instance_eval(&block) if block_given?
@html << "</#{name}>"
else
@html << " />"
end
end
def to_s
@html
end
end
def html(&block)
builder = HTMLBuilder.new
builder.instance_eval(&block)
builder.to_s
end
# Usage
page = html do
html lang: "en" do
head do
title { "My Page" }
meta charset: "utf-8"
end
body id: "main" do
h1 { "Welcome to Ruby Metaprogramming" }
p { "This is a DSL example" }
ul do
li { "First item" }
li { "Second item" }
end
end
end
end
puts page
This HTML builder DSL demonstrates the power of Ruby metaprogramming for creating expressive APIs. I’ve built similar DSLs for network configuration, test specifications, and data processing pipelines, making complex operations more readable and maintainable.
Method_missing Implementation
The method_missing
hook catches calls to undefined methods, allowing for powerful dynamic behavior.
class DataRecord
def initialize(data = {})
@data = data
end
def method_missing(name, *args)
# Check if it's a getter
if name.to_s =~ /^([a-z_]+)$/ && @data.key?($1.to_sym)
@data[$1.to_sym]
# Check if it's a setter
elsif name.to_s =~ /^([a-z_]+)=$/ && args.size == 1
@data[$1.to_sym] = args.first
# Check if it's a query method
elsif name.to_s =~ /^([a-z_]+)\?$/
!!@data[$1.to_sym]
else
super
end
end
def respond_to_missing?(name, include_private = false)
name.to_s =~ /^([a-z_]+)$/ && @data.key?($1.to_sym) ||
name.to_s =~ /^([a-z_]+)=$/ ||
name.to_s =~ /^([a-z_]+)\?$/ ||
super
end
end
user = DataRecord.new(name: "John", active: true)
puts user.name # "John"
user.email = "[email protected]"
puts user.email # "[email protected]"
puts user.active? # true
Always implement respond_to_missing?
alongside method_missing
to maintain proper object behavior. This pattern is excellent for creating flexible data objects or proxies. I’ve used it to build database record abstractions that dynamically handle column access without defining explicit methods for each field.
The key to effective use of method_missing
is ensuring you only catch methods you intend to handle and pass everything else to super
. Otherwise, debugging becomes difficult as method resolution failures won’t be reported normally.
Combining Techniques for Advanced Applications
The real power comes from combining these techniques. Consider this example of a validation framework:
module Validations
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
class_variable_set(:@@validations, {})
end
end
module ClassMethods
def validates(attribute, options = {})
validations = class_variable_get(:@@validations)
validations[attribute] ||= []
validations[attribute] << options
class_variable_set(:@@validations, validations)
end
end
def valid?
@errors = {}
validations = self.class.class_variable_get(:@@validations)
validations.each do |attribute, rules|
value = send(attribute)
rules.each do |rule|
if rule[:presence] && value.nil?
(@errors[attribute] ||= []) << "can't be blank"
end
if rule[:format] && value && !(value.to_s =~ rule[:format])
(@errors[attribute] ||= []) << "has invalid format"
end
if rule[:length] && value
if rule[:length][:minimum] && value.length < rule[:length][:minimum]
(@errors[attribute] ||= []) << "is too short (minimum: #{rule[:length][:minimum]})"
end
end
end
end
@errors.empty?
end
def errors
@errors || {}
end
end
class User
include Validations
attr_accessor :name, :email, :password
validates :name, presence: true
validates :email, presence: true, format: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :password, presence: true, length: { minimum: 8 }
def initialize(attributes = {})
attributes.each do |key, value|
send("#{key}=", value) if respond_to?("#{key}=")
end
end
end
user = User.new(name: "John", email: "invalid-email", password: "short")
puts user.valid? # false
puts user.errors # {:email=>[...], :password=>[...]}
This validation framework combines several metaprogramming techniques: module inclusion callbacks, class variable manipulation, dynamic method calls, and more. I implemented a similar system for a client that needed complex validation logic for financial data entry forms.
Performance Considerations
While metaprogramming is powerful, it comes with performance implications. Method calls through method_missing
are slower than direct method calls. Similarly, code that uses eval
can be less efficient than statically defined code.
For performance-critical sections, consider generating methods upfront rather than relying on method_missing
. Here’s an improved version of our earlier DataRecord
example:
class ImprovedDataRecord
def initialize(data = {})
@data = data
# Generate methods for initial data
generate_methods_for_data
end
def method_missing(name, *args)
# Only handle setters dynamically
if name.to_s =~ /^([a-z_]+)=$/ && args.size == 1
# Store the value
@data[$1.to_sym] = args.first
# Define the methods for this new key
define_singleton_method($1) { @data[$1.to_sym] }
define_singleton_method("#{$1}?") { !!@data[$1.to_sym] }
define_singleton_method("#{$1}=") { |v| @data[$1.to_sym] = v }
# Return the value
return args.first
end
super
end
def respond_to_missing?(name, include_private = false)
name.to_s =~ /^([a-z_]+)=$/ || super
end
private
def generate_methods_for_data
@data.each_key do |key|
define_singleton_method(key) { @data[key] }
define_singleton_method("#{key}?") { !!@data[key] }
define_singleton_method("#{key}=") { |v| @data[key] = v }
end
end
end
In production systems where I’ve used metaprogramming extensively, I’ve found this hybrid approach to be the most effective - generate methods when possible, fall back to dynamic behavior when necessary.
Final Thoughts
These seven metaprogramming techniques represent some of the most powerful capabilities in Ruby. When used judiciously, they can lead to more expressive, maintainable, and flexible code. I’ve seen metaprogramming transform complex, repetitive codebases into elegant solutions.
However, with this power comes responsibility. Code that relies heavily on metaprogramming can be harder to understand, debug, and maintain if not properly documented. As I learned from experience, always document your metaprogramming intentions clearly for future developers (including your future self).
Ruby’s metaprogramming capabilities continue to inspire me after years of working with the language. For your next Ruby project, consider how these techniques might help you create more elegant solutions to complex problems. The ability to generate code at runtime is not merely a trick—it’s a fundamental shift in how we think about programming.