Polyglot
Languages Ruby metaprogramming
Ruby § metaprogramming

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 with self set to the receiver.
  • class_eval(&block) — evaluates a block as if inside a class body.

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:

HookWhen
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_method for substantial dynamic method generation.
  • Use method_missing with respond_to_missing? — they are paired.
  • Avoid eval(str) — substantial security risk and obscurity.
  • Use instance_eval for DSL-style configuration blocks.
  • Use prepend for cross-cutting concerns (logging, caching).
  • Use hooks (inherited, included) for declarative behaviour.
  • Use send / public_send sparingly — 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.