Skip to content
AgileLogics
7 min read

Metaprogramming: send and define_method

Prerequisites: Modules and mixins, Procs and lambdas

Metaprogramming is writing code that writes or changes code while the program runs. It's how Rails turns has_many :comments into a dozen methods, and how attr_accessor creates getters and setters. Used carefully, it removes repetition; used carelessly, it makes code impossible to follow.

Calling methods by name: send and public_send

p "hello".public_send(:upcase)
p 5.public_send(:+, 3)

field = :length
p "ruby".public_send(field)
"HELLO"
8
4

public_send calls a method whose name is only known at runtime. send does the same but also calls private methods — prefer public_send unless bypassing visibility is the point (for example, in a test).

Never pass user input straight to send. obj.send(params[:action]) lets a visitor call any method, including destructive ones. Check the name against an allow-list first.

Defining methods dynamically: define_method

class Order
  STATUSES = %i[pending paid shipped cancelled].freeze

  attr_reader :status

  def initialize(status) = @status = status

  STATUSES.each do |name|
    define_method(:"#{name}?") do
      status == name
    end
  end
end

order = Order.new(:paid)
p order.paid?
p order.shipped?
p Order.instance_methods(false).sort
true
false
[:cancelled?, :paid?, :pending?, :shipped?, :status]

Four predicate methods, generated from one list. Adding a new status means adding one symbol.

Writing your own attr_-style macro

A class method that calls define_method becomes a declaration you can use in any class body — the same technique behind attr_accessor:

module Defaults
  def attr_with_default(name, default)
    define_method(name) do
      instance_variable_get(:"@#{name}") || default
    end

    define_method(:"#{name}=") do |value|
      instance_variable_set(:"@#{name}", value)
    end
  end
end

class Settings
  extend Defaults
  attr_with_default :theme, "light"
  attr_with_default :page_size, 20
end

s = Settings.new
p s.theme
s.theme = "dark"
p s.theme
p s.page_size
"light"
"dark"
20

extend Defaults adds the module's methods to the class itself, so attr_with_default can be called in the class body.

Introspection

Ruby can describe itself at runtime: obj.class, obj.methods, obj.respond_to?(:name), obj.instance_variables, Klass.instance_methods(false), Klass.ancestors, and obj.method(:name).source_location (where a method was defined — invaluable when debugging generated methods).

When not to use metaprogramming

  • When plain methods would do. Four hand-written one-line methods are easier to find (and search for) than a loop that generates them.
  • When it hides where a method comes from. If grep can't find a method's definition, document where it's generated.