Metaprogramming
What Is Metaprogramming?
Section titled “What Is Metaprogramming?”Metaprogramming is writing code that writes, modifies, or inspects code at runtime. Ruby is exceptionally well-suited for metaprogramming because:
- Classes and modules are open — they can be modified at any time
- Methods can be defined, removed, and aliased dynamically
- Every operation (including method calls and class definitions) is expressed as a message send
- Ruby’s reflective API provides deep introspection capabilities
- Blocks, procs, and lambdas are first-class objects
Open Classes
Section titled “Open Classes”Ruby classes are never closed. You can reopen and modify any class, including built-in ones:
## Reopening a classclass String def palindrome? self == reverse end
def shout upcase + "!" endend
"racecar".palindrome? # => true"hello".shout # => "HELLO!"
## Monkey patching built-in classesclass Integer def even? self % 2 == 0 end
def odd? !even? end
def hours self * 3600 end
def days self * 24 * 3600 endend
2.even? # => true3.odd? # => true5.hours # => 180002.days # => 172800Dangers of Monkey Patching
Section titled “Dangers of Monkey Patching”# Dangerous: overriding core methods affects everythingclass Array def each puts "Intercepted!" super endend
# This affects every use of Array#each in the entire program[1, 2, 3].each { |n| puts n }
# Safer alternatives: use refinements or inheritancemethod_missing
Section titled “method_missing”method_missing is Ruby’s mechanism for handling unknown method calls:
class DynamicProxy def initialize(target) @target = target end
def method_missing(name, *args, &block) if @target.respond_to?(name) @target.send(name, *args, &block) else super end end
def respond_to_missing?(name, include_private = false) @target.respond_to?(name) || super endend
# Delegation through method_missingclass Logger def initialize(target) @target = target @log = [] end
def method_missing(name, *args, &block) @log << { method: name, args: args, time: Time.now } @target.send(name, *args, &block) end
def respond_to_missing?(name, include_private = false) @target.respond_to?(name) || super end
def show_log @log.each { |entry| puts "#{entry[:time]}: #{entry[:method]}(#{entry[:args]})" } endend
array = [1, 2, 3]logged = Logger.new(array)logged.push(4)logged.lengthlogged.show_logDynamic Attribute Access
Section titled “Dynamic Attribute Access”class DynamicObject def initialize @data = {} end
def method_missing(name, *args, &block) method_name = name.to_s
if method_name.end_with?("=") @data[method_name.chomp("=")] = args.first elsif method_name.end_with?("?") !!@data[method_name.chomp("?")] elsif @data.key?(method_name) @data[method_name] else super end end
def respond_to_missing?(name, include_private = false) method_name = name.to_s method_name.end_with?("=") || method_name.end_with?("?") || @data.key?(method_name) || super end
def to_h @data.dup endend
obj = DynamicObject.newobj.name = "Alice"obj.name # => "Alice"obj.name? # => trueobj.age? # => falsedefine_method
Section titled “define_method”define_method creates methods dynamically at runtime:
class Person define_method(:name) { @name } define_method(:name=) { |value| @name = value } define_method(:greet) { |greeting = "Hello"| "#{greeting}, #{@name}" }end
p = Person.newp.name = "Alice"p.greet # => "Hello, Alice"p.greet("Hi") # => "Hi, Alice"
# Batch method definitionclass Invoice FIELDS = [:amount, :date, :customer, :paid]
FIELDS.each do |field| attr_accessor field
define_method("#{field}_changed?") do instance_variable_get("@#{field}_was") != send(field) end end
def save_changes FIELDS.each do |field| instance_variable_set("@#{field}_was", send(field)) end endend
# Dynamic method generation from a hashclass Config def self.from_hash(hash) klass = Class.new(self) do hash.each do |key, default| define_method(key) do instance_variable_get("@#{key}") || default end define_method("#{key}=") do |value| instance_variable_set("@#{key}", value) end end end klass endend
MyConfig = Config.from_hash(timeout: 30, retries: 3, host: "localhost")conf = MyConfig.newconf.timeout # => 30conf.host = "example.com"conf.host # => "example.com"eval and Binding
Section titled “eval and Binding”eval executes a string as Ruby code:
# Basic evalresult = eval("2 + 3") # => 5
# Eval with bindingx = 10eval("x + 5") # => 15
# Eval with different bindingclass A def initialize @value = 42 endend
a = A.neweval("@value", a.instance_eval { binding })# => 42
# DANGERS of evaluser_input = gets.chompeval(user_input) # SECURITY RISK: arbitrary code execution!
# Safer alternatives# Use send, public_send, define_method instead of evalBinding Objects
Section titled “Binding Objects”A Binding object captures the entire execution context at a point in time:
def create_multiplier(factor) bindingend
b = create_multiplier(5)eval("factor * 10", b) # => 50
# Practical use: capturing context for later evaluationclass Template def initialize(source) @source = source end
def render(context) context.instance_eval(@source) endend
class ViewContext attr_accessor :title, :items
def initialize @title = "Default" @items = [] end
def render_partial(name) "<partial:#{name}>" endend
ctx = ViewContext.newctx.title = "My Page"ctx.items = [1, 2, 3]
template = Template.new('"<h1>#{title}</h1><p>Items: #{items.size}</p>"')puts template.render(ctx)# => "<h1>My Page</h1><p>Items: 3</p>"TOPLEVEL_BINDING
Section titled “TOPLEVEL_BINDING”# TOPLEVEL_BINDING captures the top-level contextx = 100
Thread.new do eval("x", TOPLEVEL_BINDING) # => 100end.joinsend and public_send
Section titled “send and public_send”send calls a method by name (as a symbol or string):
class User attr_accessor :name, :email
def greet "Hello, I'm #{@name}" end
private
def secret_key "abc123" endend
user = User.newuser.name = "Alice"
# send calls any method, including private onesuser.send(:name) # => "Alice"user.send(:secret_key) # => "abc123"
# public_send only calls public methodsuser.public_send(:name) # => "Alice"user.public_send(:secret_key) # => NoMethodError (private method)
# Dynamic method dispatchmethod_name = :greetuser.send(method_name) # => "Hello, I'm Alice"
# Dynamic dispatch with argumentsusers.each do |u| method_to_call = u.admin? ? :admin_greeting : :standard_greeting u.send(method_to_call)end
# Mass assignment patternattributes = { name: "Bob", email: "bob@example.com" }attributes.each do |key, value| user.send("#{key}=", value)end
# send with a blockarray = [3, 1, 4, 1, 5]array.send(:sort_by) { |n| -n } # => [5, 4, 3, 1, 1]send (safe alternative)
Section titled “send (safe alternative)”__send__ is the safe version of send that cannot be overridden:
# If someone overrides sendclass Deceptive def send(method, *args) puts "Intercepted!" endend
Deceptive.new.send(:to_s) # => "Intercepted!"Deceptive.new.__send__(:to_s) # => safe, calls actual methodrespond_to?
Section titled “respond_to?”Check whether an object responds to a method before calling it:
obj = "hello"
obj.respond_to?(:length) # => trueobj.respond_to?(:nonexistent) # => falseobj.respond_to?(:send) # => true (inherited from Object)
# Check if method is publicobj.respond_to?(:send, true) # => false (send is private-ish)
# Duck typing with respond_to?def process(data) if data.respond_to?(:each) data.each { |item| puts item } elsif data.respond_to?(:to_s) puts data.to_s else raise "Cannot process #{data.inspect}" endend
process([1, 2, 3]) # prints 1, 2, 3process("hello") # prints "hello"process(42) # raises errorclass_eval and instance_eval
Section titled “class_eval and instance_eval”class_eval (Module#class_eval)
Section titled “class_eval (Module#class_eval)”Evaluates a block in the context of a class, defining class-level methods and constants:
class Person attr_reader :nameend
Person.class_eval do def greet "Hello, #{@name}" end
def self.create(name) new(name) endend
Person.create("Alice").greet # => "Hello, Alice"
# Dynamic class modificationclass_name = "Product"fields = [:name, :price, :stock]
klass = Class.new do fields.each do |field| attr_accessor field endend
Object.const_set(class_name, klass)
product = Product.newproduct.name = "Widget"product.price = 9.99instance_eval (Object#instance_eval)
Section titled “instance_eval (Object#instance_eval)”Evaluates a block in the context of an instance, accessing private state:
class Secret def initialize @value = 42 end
private
def internal_method @value * 2 endend
s = Secret.new
# instance_eval gives access to private methods and instance variabless.instance_eval do @value # => 42 internal_method # => 84end
# instance_eval for singleton method definitionobj = "hello"obj.instance_eval do def shout upcase + "!!!" endend
obj.shout # => "HELLO!!!"
# instance_eval on a class defines singleton methods (= class methods)Person = Class.new do instance_eval do define_method(:new_method) do "instance method" end endendMethod Aliases
Section titled “Method Aliases”alias and alias_method
Section titled “alias and alias_method”# alias (keyword) -- creates a method alias at class definition timeclass String alias :sentence_case :capitalize alias :word_count :lengthend
"hello".sentence_case # => "Hello""hello".word_count # => 5
# alias_method -- can be called at any timeclass Array alias_method :second, :atend
[10, 20, 30].second(1) # => 20
# Chaining with super via alias_methodclass Greeting def hello "Hello" end
def hello_with_name(name) "#{hello}, #{name}!" endend
# Wrap original methodclass Greeting alias_method :hello_original, :hello
def hello "#{hello_original} (enhanced)" endend
Greeting.new.hello # => "Hello (enhanced)"Method Wrapping Pattern
Section titled “Method Wrapping Pattern”module MethodWrapper def wrap_method(method_name) original = instance_method(method_name) define_method(method_name) do |*args, &block| puts "Before: #{method_name}" result = original.bind(self).call(*args, &block) puts "After: #{method_name}" result end endend
class Calculator include MethodWrapper
def add(a, b) a + b end
wrap_method :addend
Calculator.new.add(2, 3)# => "Before: add"# => "After: add"# => 5Method Introspection
Section titled “Method Introspection”Ruby provides extensive facilities for examining methods at runtime:
class Example def public_method; end protected :protected_method private :private_method
def self.class_method; endend
# Instance methodsExample.instance_methods(false)# => [:public_method]
Example.public_instance_methods(false)# => [:public_method]
Example.protected_instance_methods(false)# => [:protected_method]
Example.private_instance_methods(false)# => [:private_method]
# Class methods (singleton methods)Example.singleton_methods# => [:class_method]
# Method objectsm = Example.instance_method(:public_method)m.name # => :public_methodm.arity # => 0m.owner # => Examplem.parameters # => []
# Object method lookupobj = Example.newobj.method(:public_method) # => Method objectobj.public_method(:public_method) # => sameobj.public_send(:public_method)
# Defined methodsExample.method_defined?(:public_method) # => trueExample.public_method_defined?(:public_method) # => trueExample.private_method_defined?(:private_method) # => true
# respond_to?obj.respond_to?(:public_method) # => trueobj.respond_to?(:private_method) # => false (without include_all)obj.respond_to?(:private_method, true) # => true (includes private)
# Method source location (MRI only)Example.instance_method(:public_method).source_location# => ["/path/to/file.rb", line_number]
# Methods from ancestorsExample.ancestors# => [Example, Object, Kernel, BasicObject]
# is_a? and kind_of?obj.is_a?(Example) # => trueobj.is_a?(Object) # => trueobj.kind_of?(Example) # => trueRemoving and Undefining Methods
Section titled “Removing and Undefining Methods”class Example def method_a; puts "A"; end def method_b; puts "B"; endend
# remove_method: removes the method from this class only# Parent class method is still accessibleclass Example remove_method :method_aend
# undef_method: prevents any call to this method (even from superclasses)class Example undef_method :method_bend
# Practical: prevent certain methodsclass SensitiveData undef_method :inspect, :to_s
def initialize(data) @data = data endendRefinements
Section titled “Refinements”Refinements provide scoped monkey patching — modifications are only visible within a specific scope:
# Define a refinementmodule StringExtensions refine String do def pluralize self + "s" end
def sentence_case capitalize end endend
# Without using, the refinement is not active"cat".pluralize # => NoMethodError
# Using the refinement in a specific scopeclass Report using StringExtensions
def generate(title) title.pluralize # works here title.sentence_case # works here endend
# Outside the using scope"cat".pluralize # => NoMethodError (still not available)
# Refinements are lexicalmodule DataProcessor using StringExtensions
def self.process(word) word.pluralize # works end
def self.another_module # using is active here (nested in DataProcessor) "dog".pluralize # works endend
class OutsideClass # using is NOT active here def process "cat".pluralize # NoMethodError endend
# Refinements with multiple modulesmodule IntegerPatches refine Integer do def weeks self * 7 end
def ago Time.now - self * 86400 end endend
class TimeTracker using IntegerPatches
def self.report puts "#{3.weeks} days is #{3.weeks.ago}" endendClass-Level Metaprogramming
Section titled “Class-Level Metaprogramming”const_missing
Section titled “const_missing”module Config def self.const_missing(name) path = "config/#{name.to_s.downcase}.yml" if File.exist?(path) data = YAML.safe_load(File.read(path)) const_set(name, data) else super end endend
# First access loads the constantConfig.database # loads config/database.yml and caches itConfig.database # returns cached valueconst_set and const_get
Section titled “const_set and const_get”class Version MAJOR = 1 MINOR = 2 PATCH = 3end
Version.const_get(:MAJOR) # => 1Version.const_set(:FULL, "1.2.3")
# Dynamic constant definitionmodule Registry def self.register(name, klass) const_set(name, klass) endend
class MyService; endRegistry.register(:Service, MyService)Registry::Service # => MyService
# List constantsVersion.constants # => [:MAJOR, :MINOR, :PATCH, :FULL]Version.constants(false) # => own constants onlyFreezing Classes
Section titled “Freezing Classes”# Freeze a class to prevent further modificationsclass Immutable def method_a; endend
Immutable.freeze
class Immutable def method_b; endend # => FrozenError: can't modify frozen class
# Practical use: freeze classes after boot# Rails uses this pattern in productionif Rails.env.production? ApplicationRecord.descendants.each(&:freeze)endPractical Metaprogramming Patterns
Section titled “Practical Metaprogramming Patterns”DSL Construction
Section titled “DSL Construction”class RouteSet def initialize @routes = [] end
def get(path, to:) @routes << { method: :GET, path: path, handler: to } end
def post(path, to:) @routes << { method: :POST, path: path, handler: to } end
def match(method, path) route = @routes.find { |r| r[:path] == path && r[:method] == method } route&.dig(:handler) end
def routes @routes.dup endend
# Using the DSLrouter = RouteSet.newrouter.get("/users", to: UsersController.action(:index))router.get("/users/:id", to: UsersController.action(:show))router.post("/users", to: UsersController.action(:create))
# Builder patternclass HTML def initialize @content = "" end
def tag(name, **attrs, &block) @content << "<#{name}" attrs.each { |k, v| @content << " #{k}=\"#{v}\"" } @content << ">" @content << block.call if block_given? @content << "</#{name}>" self end
def text(str) @content << str self end
def to_s @content endend
HTML.new.tag(:div, class: "main") do HTML.new.tag(:h1) { "Title" }.to_send.to_sDelegation
Section titled “Delegation”# Forwardable module for clean delegationrequire 'forwardable'
class Employee extend Forwardable
def initialize @contact_info = ContactInfo.new @work_info = WorkInfo.new end
def_delegators :@contact_info, :email, :phone, :address def_delegator :@work_info, :title, :job_title def_delegators :@work_info, :department, :salaryend
## Intuition
Ruby metaprogramming is like being a playwright who writes new scenes during the performance. When you define a method dynamically with define_method, you are creating a new role for an actor while the show is running. This power comes with responsibility: if you are not careful, you can confuse the stage crew (debugging tools) and the audience (other developers).
Method_missing is like a catch-all response. When an object receives a message it does not understand, method_missing is the fallback handler. This is like having a translator who can improvise when someone speaks a language you do not know. The translator might not get it exactly right, but they can in most cases figure out what you mean from context.
## Worked Examples
### Example 1: Dynamic Validations with `define_method` and `method_missing`
**Problem:** Build a model-like class that dynamically defines attribute readers, writers, and presence validations from a schema definition.
```rubyclass DynamicModel def self.schema(&block) @schema ||= {} instance_eval(&block) if block @schema end
def self.attribute(name, type: :string, validates: {}) @schema[name] = { type: type, validates: validates }
define_method(name) { instance_variable_get(:"@#{name}") } define_method(:"#{name}=") { |val| instance_variable_set(:"@#{name}", val) }
if validates[:presence] define_method(:"validate_#{name}") do val = send(name) errors << "#{name} is required" if val.nil? || (val.respond_to?(:empty?) && val.empty?) end end end
def initialize(attrs = {}) @errors = [] attrs.each { |k, v| send(:"#{k}=", v) if respond_to?(:"#{k}=") } end
attr_reader :errors
def valid? @errors.clear self.class.schema.each do |name, config| send(:"validate_#{name}") if config[:validates][:presence] end @errors.empty? end
def method_missing(name, *args) if name.to_s.start_with?("validate_") # No validation defined for this attribute nil else super end end
def respond_to_missing?(name, include_private = false) name.to_s.start_with?("validate_") || super endend
class User < DynamicModel schema do attribute :name, validates: { presence: true } attribute :email, validates: { presence: true } attribute :age, type: :integer endend
user = User.new(name: "Alice", email: "alice@example.com")puts user.valid? # => trueputs user.name # => "Alice"
empty_user = User.newputs empty_user.valid? # => falseputs empty_user.errors # => ["name is required", "email is required"]Explanation: schema uses instance_eval to run the block in the class context. attribute dynamically creates getter/setter methods via define_method and stores validation rules. valid? iterates through the schema and calls each validation method. method_missing handles undefined validation methods gracefully.
Example 2: Method Wrapping with alias_method and define_method
Section titled “Example 2: Method Wrapping with alias_method and define_method”Problem: Create a timing decorator that can be applied to any method, measuring execution time without modifying the original method.
module Timed def timed(method_name) original = instance_method(method_name) timing_data = {}
define_method(method_name) do |*args, &block| start = Process.clock_gettime(Process::CLOCK_MONOTONIC) result = original.bind(self).call(*args, &block) elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
key = "#{self.class.name}##{method_name}" timing_data[key] ||= { calls: 0, total_time: 0 } timing_data[key][:calls] += 1 timing_data[key][:total_time] += elapsed
puts "[TIMED] #{key}: #{elapsed.round(4)}s" result end
define_method(:"#{method_name}_timings") do timing_data[ "#{self.class.name}##{method_name}"] end endend
class DataProcessor include Timed
timed def process_batch(batch) batch.map { |item| item * 2 }.select(&:even?) end
timed def slow_operation sleep(0.01) "done" endend
processor = DataProcessor.newprocessor.process_batch([1, 2, 3, 4, 5])processor.slow_operation
puts processor.process_batch_timings# => {:calls=>1, :total_time=>0.000123...}Explanation: instance_method captures the original method as an UnboundMethod. define_method creates a new method that times execution using monotonic clocks. original.bind(self).call invokes the original method. The :timed class method acts as a declarative decorator.
Example 3: Dynamic Module Inclusion with eval
Section titled “Example 3: Dynamic Module Inclusion with eval”Problem: Generate specialized query methods from a configuration hash at runtime.
module QueryBuilder def self.build(queries) mod = Module.new
queries.each do |name, conditions| mod.define_method(name) do result = all conditions.each do |field, value| result = result.select { |r| r[field] == value } end result end end
mod endend
class ProductStore attr_reader :products
def initialize(products) @products = products end
include QueryBuilder.build({ active_electronics: { category: "electronics", active: true }, expensive_clothing: { category: "clothing", price: ->(p) { p > 100 } }, recently_added: { created_at: ->(t) { t > 7.days.ago } } })
def all products endend
store = ProductStore.new([ { name: "Phone", category: "electronics", active: true, created_at: Time.current }, { name: "Shirt", category: "clothing", price: 200, created_at: 14.days.ago }, { name: "Laptop", category: "electronics", active: true, created_at: 3.days.ago }])
puts store.active_electronics# => [{name: "Phone", ...}, {name: "Laptop", ...}]Explanation: Module.new creates an anonymous module. define_method adds query methods that filter products based on conditions. Lambda conditions (like price: ->(p) { p > 100 }) are called dynamically. The module is then included in the class, making the query methods available as instance methods.
class ContactInfo attr_accessor :email, :phone, :address def initialize @email = “a@b.com” @phone = “555-1234” end end
class WorkInfo attr_accessor :title, :department, :salary def initialize @title = “Engineer” end end
emp = Employee.new emp.email # => “a@b.com” emp.job_title # => “Engineer”
## Cross-References
- [OOP](https://languages.wyattau.com/ruby/04-oop/1_oop/) - How metaprogramming extends Ruby's object model with dynamic behavior- [Methods and Blocks](https://languages.wyattau.com/ruby/03-methods-blocks/1_methods-and-blocks/) - How define_method and method_missing dynamically create method dispatch- [Concurrency](/05-advanced/2_concurrency/) - How thread safety concerns affect metaprogrammed code
## Common Mistakes
**Using `method_missing` without defining `respond_to_missing?`.** When you override `method_missing`, you should also override `respond_to_missing?` to return true for the dynamically handled methods. Without this, `obj.respond_to?(:dynamic_method)` returns false even though the method works.
**Overusing `eval` and `class_eval` with string interpolation.** `eval("def #{name}")` executes arbitrary string code, which is slow and poses security risks. Prefer `define_method` for dynamic method creation, which is safer and faster. Use `class_eval` with blocks instead of strings when possible.
**Forgetting that metaprogramming happens at compile time.** Macros and `define_method` execute when the class is loaded, not when methods are called. Students sometimes expect runtime values in metaprogrammed code, which only has access to compile-time information like module attributes.