Ruby Metaprogramming Cheat Sheet
Covers method_missing, define_method, class_eval/instance_eval, hooks like included/inherited, and building simple DSLs in idiomatic Ruby.
Dynamically Defining Methods
`define_method` creates methods at runtime, closing over local variables.
class Product ATTRS = [:name, :price, :sku] ATTRS.each do |attr| define_method(attr) { instance_variable_get("@#{attr}") } define_method("#{attr}=") { |val| instance_variable_set("@#{attr}", val) } endendp = Product.newp.name = "Widget"p.name # => "Widget"
`method_missing` & `respond_to_missing?`
Intercept calls to undefined methods — always pair with respond_to_missing? for correctness.
class DynamicProxy def initialize(data) @data = data end def method_missing(name, *args) key = name.to_s if key.end_with?('=') @data[key.chomp('=')] = args.first elsif @data.key?(key) @data[key] else super # important: fall back for truly unknown methods end end def respond_to_missing?(name, include_private = false) @data.key?(name.to_s.chomp('=')) || super endendobj = DynamicProxy.new({'title' => 'Ruby'})obj.title # => "Ruby"obj.respond_to?(:title) # => true
`class_eval`, `instance_eval` & Hooks
Reopen classes at runtime and hook into inheritance/inclusion events.
class Base def self.inherited(subclass) puts "#{subclass} inherited from #{self}" endendmodule Trackable def self.included(base) puts "Trackable included in #{base}" base.extend(ClassMethods) end module ClassMethods def track(attr) define_method("track_#{attr}") { send(attr) } end endendString.class_eval do def shout upcase + "!" endend"hi".shout # => "HI!"
Building a Small DSL
Combine `instance_eval` with blocks to get clean, declarative-looking configuration syntax.
class Router def initialize(&block) @routes = [] instance_eval(&block) if block_given? end def get(path, to:) @routes << { verb: :get, path: path, handler: to } end def routes @routes endendrouter = Router.new do get '/users', to: 'users#index' get '/users/:id', to: 'users#show'endrouter.routes # => [{verb: :get, path: "/users", ...}, ...]
Core Metaprogramming Hooks
Reference for the most-used reflective methods.
- method_missing- intercepts calls to undefined methods on an object
- respond_to_missing?- must be overridden alongside method_missing for respond_to? correctness
- define_method- defines a method at runtime, can close over local variables
- class_eval / instance_eval- evaluate a block/string in the context of a class or object
- included / extended / inherited- module/class hooks fired on include, extend, subclassing
- send / public_send- invoke a method by symbol name, bypassing/respecting visibility
- instance_variable_get/set- read/write ivars dynamically by name
`Module#prepend` vs `refine`
prepend inserts a module ABOVE a class in the ancestor chain so `super` reaches the original method; refinements scope changes lexically instead of globally.
module Loud def greet(name) super(name).upcase endendclass Greeter def greet(name) = "hello, #{name}" prepend Loud # Loud now sits above Greeter in the method resolution orderendGreeter.new.greet("ada") # => "HELLO, ADA"Greeter.ancestors.first(2) # => [Loud, Greeter]# Refinements only apply where `using` is explicitly calledmodule StringRefinement refine String do def shout = upcase + "!" endendusing StringRefinement"hi".shout # => "HI!" -- invisible to any file that didn't call `using`
Singleton Classes & `class << self`
Every object has a hidden singleton class holding its own-only methods — that's what `class << self` opens.
obj = Object.newdef obj.special "only this object has this method"endobj.singleton_class # => #<Class:#<Object:0x...>>obj.singleton_methods # => [:special]class Widget class << self # equivalent to a series of `def self.method_name` def create(name) new.tap { |w| w.name = name } end attr_accessor :registry endend
`const_missing` & a Pure `BasicObject` Proxy
const_missing lazily resolves undefined constants; BasicObject strips nearly all inherited methods for building transparent proxies.
class LazyConstants def self.const_missing(name) value = load_from_config(name) const_set(name, value) # cache so const_missing won't fire again end def self.load_from_config(name) { API_KEY: "abc123" }.fetch(name) endendLazyConstants::API_KEY # triggers const_missing once, then reads the cached constantclass Proxy < BasicObject def initialize(target) @target = target end def method_missing(name, *args, &block) @target.send(name, *args, &block) end def respond_to_missing?(name, include_private = false) @target.respond_to?(name, include_private) endend
`Method` / `UnboundMethod` Objects
Detach a method from its class, inspect it, and rebind it onto a compatible object at runtime.
class Base def greet = "hi from #{self.class}"endunbound = Base.instance_method(:greet)class Other; endbound = unbound.bind(Other.new) # rebind onto any object whose class is compatiblebound.call # => "hi from Other"m = Base.new.method(:greet)m.arity # => 0m.owner # => Basem.source_location # => ["file.rb", 1]m.unbind # back to an UnboundMethod
Advanced Metaprogramming Hooks
Reflective and lexically-scoped tools beyond the basic method_missing/define_method toolkit.
- Module#prepend- inserts a module ABOVE a class in the ancestor chain, letting it wrap/override with access to `super`
- refine / using- lexically-scoped monkey patches visible only in files/scopes that explicitly call `using`
- const_missing- class hook fired when an undefined constant is referenced on that class or module
- method_added / method_removed- class hooks fired whenever an instance method is defined or removed, useful for DSL bookkeeping
- singleton_class- the hidden class holding an object's own methods; what `class << self` reopens
- TracePoint- low-level hook for tracing method calls, line execution, and exceptions at runtime
- ObjectSpace- iterate over all live objects of a given class, handy for debugging dynamically generated methods
- Kernel#binding- captures the current scope (locals + self) as a Binding object for later eval
Prefer define_method over method_missing whenever the set of dynamic methods is known ahead of time (e.g. from a list of attributes) — it's faster, shows up correctly in respond_to? without extra code, and gives clean backtraces.