Metaprogramming
Metaprogramming — code that operates on code — is one of Ruby’s defining features. The principal mechanisms: open classes (existing classes may be reopened and extended), dynamic method definition (define_method, attr_*), method dispatch interception (method_missing, respond_to_missing?), the eval family (eval, instance_eval, class_eval), introspection (methods, instance_variables, ancestors), and hooks (included, inherited, method_added). The combination admits substantial DSLs (Rails, RSpec, Sinatra), automatic accessor generation, declarative configuration, and runtime code generation. The conventional contemporary discipline reserves elaborate metaprogramming for libraries; ordinary code uses the substantial built-in idioms (attr_*, Forwardable, modules) without elaborate metaprogramming.
Open classes
Existing classes may be reopened:
class String
def shout
upcase + "!"
end
end
"hello".shout # "HELLO!"
The mechanism admits monkey patching — modifying built-in or third-party classes. The conventional contemporary discipline:
- Acceptable: extending your own classes across files.
- Conventional: the active_support extensions in Rails (
String#blank?, etc.). - Risky: modifying classes from third-party libraries.
- Avoid: modifying core classes for narrow purposes.
For lexically-scoped patches, refinements admit substantial discipline:
module StringPatch
refine String do
def shout
upcase + "!"
end
end
end
class MyClass
using StringPatch
# shout is admitted here only
end
Treated in Modules and mixins.
Dynamic method definition
The define_method admits defining methods at runtime:
class Config
[:host, :port, :timeout].each do |attr|
define_method(attr) { instance_variable_get("@#{attr}") }
define_method("#{attr}=") do |value|
instance_variable_set("@#{attr}", value)
end
end
end
c = Config.new
c.host = "localhost"
c.port = 8080
puts c.host # "localhost"
The conventional attr_accessor is itself implemented this way; it’s a class method that calls define_method for the getter and setter.
The define_method accepts a block (or a Proc), capturing the surrounding scope:
class API
ENDPOINTS = {
users: "/api/users",
posts: "/api/posts",
comments: "/api/comments",
}
ENDPOINTS.each do |name, path|
define_method("get_#{name}") do
fetch(path)
end
end
end
api = API.new
api.get_users # calls fetch("/api/users")
method_missing
When a method is called on an object that does not respond to it, Ruby calls method_missing:
class Echo
def method_missing(name, *args, **kwargs, &block)
puts "called #{name} with #{args.inspect}"
end
end
e = Echo.new
e.anything(1, 2, 3) # "called anything with [1, 2, 3]"
e.foo # "called foo with []"
The mechanism admits substantial dynamism — proxies, DSLs, ORM-style accessors, etc.
For type-correctness, override respond_to_missing? whenever overriding method_missing:
class Echo
def method_missing(name, *args, **kwargs, &block)
if name.to_s.start_with?("echo_")
args.first.to_s.upcase
else
super
end
end
def respond_to_missing?(name, include_private = false)
name.to_s.start_with?("echo_") || super
end
end
e = Echo.new
e.echo_hello("hi") # "HI"
e.respond_to?(:echo_anything) # true (with respond_to_missing?)
e.unknown_method # NoMethodError
The super in method_missing admits genuine “no such method” errors for non-handled cases.
eval family
Several eval variants admit running strings or blocks as code:
# eval — string evaluation in the current binding:
x = 5
eval("x + 1") # 6
# instance_eval — evaluate with self set to the receiver:
"hello".instance_eval { upcase } # "HELLO"
# class_eval — evaluate inside a class definition:
String.class_eval do
def shout
upcase + "!"
end
end
# instance_exec / class_exec — like instance_eval but admits arguments:
[1, 2].instance_exec(10) { |n| sum + n }
The principal differences:
eval(str)— evaluates a string; the most powerful and most dangerous form.instance_eval(&block)— evaluates a block withselfset to the receiver.class_eval(&block)— evaluates a block as if inside aclassbody.
The eval(str) form admits substantial security risks; the conventional discipline avoids it for user-supplied input. The block forms (instance_eval, class_eval) are conventional for DSLs.
DSL via instance_eval
The instance_eval is the principal building block for DSLs:
class HTMLBuilder
def initialize(&block)
@output = ""
instance_eval(&block) if block
end
def h1(text)
@output += "<h1>#{text}</h1>"
end
def p(text)
@output += "<p>#{text}</p>"
end
def to_s
@output
end
end
html = HTMLBuilder.new do
h1 "Welcome"
p "Hello, world"
end
puts html
# <h1>Welcome</h1><p>Hello, world</p>
The mechanism admits substantial natural-syntax configuration; conventional in Rails routes, RSpec specs, Rake tasks, etc.
Introspection
Ruby admits substantial runtime introspection:
"hello".class # String
"hello".methods.size # ~180
"hello".methods.grep(/case/) # methods matching /case/
"hello".respond_to?(:upcase) # true
"hello".instance_variables # []
String.ancestors # [String, Comparable, Object, Kernel, BasicObject]
String.instance_methods(false) # methods defined directly on String
# Method objects:
m = "hello".method(:upcase)
m.call # "HELLO"
# Defined source:
m.source_location # ["/path/to/source.rb", 123]
# Method introspection:
String.instance_method(:upcase).arity # 0 (no required args)
The mechanism admits substantial reflection — the conventional Ruby debugging and IDE tooling leans on these facilities.
Hooks
Several methods are callbacks run automatically:
class Tracked
def self.inherited(subclass)
puts "#{subclass} inherited from Tracked"
end
def self.method_added(name)
puts "method #{name} added"
end
end
class Concrete < Tracked # "Concrete inherited from Tracked"
def foo # "method foo added"
end
end
module M
def self.included(base)
puts "M was included into #{base}"
end
def self.extended(base)
puts "M was extended into #{base}"
end
end
The principal hooks:
| Hook | When |
|---|---|
inherited(subclass) | A subclass extends this class |
included(base) | This module is included |
extended(base) | This module is extended |
prepended(base) | This module is prepended |
method_added(name) | An instance method is added |
method_removed(name) | An instance method is removed |
singleton_method_added(name) | A class/singleton method is added |
at_exit { ... } | The program exits |
The hooks admit substantial declarative behaviour; conventional in framework code.
class_eval and instance_eval for DSLs
Two distinct uses:
class String
# Add a method to String:
String.class_eval do
def shout
upcase + "!"
end
end
end
# Equivalent without class_eval:
class String
def shout
upcase + "!"
end
end
# instance_eval — modify a single object:
obj = Object.new
obj.instance_eval do
def special
"special"
end
end
obj.special # "special"
# obj.class.new.special # NoMethodError (only on this object)
Singleton classes
Each object has a singleton class — admitting per-object methods:
obj = Object.new
def obj.unique_method
"only on this object"
end
obj.unique_method # OK
obj.singleton_class # the singleton class
# Or:
class << obj
def another_unique
"also only on this object"
end
end
For classes, class << self admits adding class methods:
class Foo
class << self
def class_method # equivalent to `def self.class_method`
# ...
end
attr_accessor :class_attribute
end
end
Foo.class_method
Foo.class_attribute = "value"
send and public_send
The send admits invoking a method by name:
"hello".send(:upcase) # "HELLO"
"hello".send(:upcase!) # mutates
arr.send(:push, 1, 2, 3)
# By default, send admits private methods:
class Foo
private
def secret; "secret"; end
end
Foo.new.send(:secret) # "secret"
Foo.new.public_send(:secret) # NoMethodError
The public_send is the conventional form when respect for visibility matters; send is conventional for substantial metaprogramming.
Procs as methods
A Proc may be used to define methods:
class Foo
helper = ->(name) { "Hello, #{name}" }
define_method(:greet, &helper)
end
Foo.new.greet("Alice") # "Hello, Alice"
The mechanism admits closures over surrounding scope; conventional for DSLs.
Common patterns
Auto-generated accessors
class Configuration
ATTRIBUTES = [:host, :port, :timeout, :retries]
ATTRIBUTES.each do |attr|
attr_accessor attr
end
def initialize(**opts)
opts.each { |k, v| send("#{k}=", v) }
end
end
c = Configuration.new(host: "localhost", port: 8080)
Method-missing proxy
class Proxy
def initialize(target)
@target = target
end
def method_missing(name, *args, **kwargs, &block)
puts "calling #{name}"
@target.public_send(name, *args, **kwargs, &block)
end
def respond_to_missing?(name, include_private = false)
@target.respond_to?(name, include_private)
end
end
p = Proxy.new("hello")
p.upcase # logs and returns "HELLO"
Decorator via prepend
module Logging
def calculate
puts "before calculate"
result = super
puts "after calculate (got #{result})"
result
end
end
class Service
prepend Logging
def calculate
42
end
end
Service.new.calculate
# before calculate
# after calculate (got 42)
# => 42
DSL with instance_eval
class Route
attr_reader :method, :path, :handler
def initialize(&block)
instance_eval(&block)
end
def get(path, &handler)
@method = :get
@path = path
@handler = handler
end
end
route = Route.new do
get "/users" do
"list users"
end
end
route.handler.call # "list users"
Hooks for plugin registration
class Plugin
@registry = []
def self.inherited(subclass)
@registry << subclass
end
def self.registry
@registry
end
end
class HtmlPlugin < Plugin; end
class CssPlugin < Plugin; end
Plugin.registry # [HtmlPlugin, CssPlugin]
The pattern admits automatic registration of subclasses.
Tagged hashes via method_missing
class Hash
def method_missing(name, *args)
if key?(name)
self[name]
elsif key?(name.to_s)
self[name.to_s]
else
super
end
end
def respond_to_missing?(name, include_private = false)
key?(name) || key?(name.to_s) || super
end
end
h = { name: "Alice", age: 30 }
h.name # "Alice"
h.age # 30
The pattern is admitted but conventionally avoided — it modifies a core class globally; refinements are the disciplined alternative.
Class with attribute introspection
class Model
def self.attribute(name, type)
@attributes ||= {}
@attributes[name] = type
attr_accessor name
end
def self.attributes
@attributes
end
end
class User < Model
attribute :name, :string
attribute :age, :integer
end
User.attributes # {name: :string, age: :integer}
Method aliasing
class Calculator
def add(a, b)
a + b
end
alias_method :sum, :add
end
c = Calculator.new
c.add(1, 2) # 3
c.sum(1, 2) # 3
The alias_method (or the alias keyword) admits naming the same method by another identifier.
Around-method via prepend
module CacheAround
def expensive_compute(*args)
@cache ||= {}
@cache[args] ||= super
end
end
class Service
prepend CacheAround
def expensive_compute(x)
sleep 1
x * x
end
end
s = Service.new
s.expensive_compute(5) # takes 1 second; returns 25
s.expensive_compute(5) # cached; instantaneous
A note on the conventional discipline
The contemporary Ruby metaprogramming advice:
- Use ordinary Ruby when possible —
attr_*,Forwardable, mixins, blocks. - Use
define_methodfor substantial dynamic method generation. - Use
method_missingwithrespond_to_missing?— they are paired. - Avoid
eval(str)— substantial security risk and obscurity. - Use
instance_evalfor DSL-style configuration blocks. - Use
prependfor cross-cutting concerns (logging, caching). - Use hooks (
inherited,included) for declarative behaviour. - Use
send/public_sendsparingly — explicit method calls are conventionally clearer. - Use refinements over open classes for scoped patches.
- Document elaborate metaprogramming — it is conventionally non-obvious.
- Reserve elaborate metaprogramming for libraries — application code should be straightforward.
The combination — open classes, dynamic method definition, method_missing, the eval family, hooks, the substantial introspection facilities — is the substance of Ruby’s metaprogramming surface. The discipline produces substantial expressiveness for library authors; for application code, restraint is conventional. The mechanism powers Ruby’s distinctive DSLs (Rails, RSpec, Sinatra) and admits substantial declarative configuration patterns that would otherwise require explicit infrastructure in less-dynamic languages.