Metaprogramming | Ruby - Wyatt's Notes
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
```mermaidflowchart TD A[1_Metaprogramming] --> B[Key Concepts] A --> C[Core Principles] A --> D[Practical Applications] B --> E[Fundamental definitions] C --> F[Design patterns] D --> G[Real-world usage]Intuition
Section titled “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
Section titled “Worked Examples”Example 1: Dynamic Validations with define_method and method_missing
Section titled “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.
class 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](../../../../../languages/src/content/docs/ruby/04-oop/1_oop) - How metaprogramming extends Ruby's object model with dynamic behavior- [Methods and Blocks](../../../../../languages/src/content/docs/ruby/03-methods-blocks/1_methods-and-blocks) - How define_method and method_missing dynamically create method dispatch- [Concurrency](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.