Metaprogramming
Intuition
Section titled “Intuition”Metaprogramming is code that writes or modifies code. In Ruby, everything is an object and every operation is a message send, making the language highly reflective. Open classes let you modify existing types at runtime, and method_missing catches calls to undefined methods, enabling dynamic dispatch. These techniques are powerful but risky: modifying core classes affects the entire program. Refinements provide a safer alternative by limiting monkey patching to specific lexical scopes.
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
class ContactInfo attr_accessor :email, :phone, :address def initialize @email = "a@b.com" @phone = "555-1234" endend
class WorkInfo attr_accessor :title, :department, :salary def initialize @title = "Engineer" endend
emp = Employee.newemp.email # => "a@b.com"emp.job_title # => "Engineer"Cross-References
Section titled “Cross-References”- Object-Oriented Programming provides the class and object foundation that metaprogramming dynamically modifies at runtime.
- Methods and Blocks covers the method resolution order and block semantics that metaprogramming hooks into.
- Concurrency addresses thread safety concerns that arise when metaprogramming modifies shared state.
Common Mistakes
Section titled “Common Mistakes”Monkey patching core classes globally: Adding methods to String, Array, or Integer affects the entire program and can cause unexpected conflicts with gems. Use refinements instead to limit scope.
Forgetting to define respond_to_missing?: If you override method_missing but not respond_to_missing?, respond_to? returns false for methods your proxy handles, breaking duck typing.
Using eval with user input: eval executes arbitrary Ruby code, creating a critical security vulnerability. Use send, public_send, or define_method instead for dynamic dispatch.