Metaprogramming

Ruby · Advanced · 16 min

Metaprogramming is behind attr_accessor, ActiveRecord attributes and most Ruby DSLs.

Ruby lets code write code at runtime, and the core tools are ordinary methods.

  • public_send(:name, *args) calls a method chosen at runtime. send does the same but also reaches private methods, so prefer public_send.
  • define_method(:name) { |args| ... } creates a method from a block. Because the block is a closure, generated methods can capture loop variables, which is how attr_accessor-style macros are built.
  • instance_variable_get(:@x) and instance_variable_set read and write state by name.
  • respond_to?(:name) and method_defined? check for methods.

A class macro is just a class method called in the class body. When has_flag :admin runs, self is the class, so the macro can call define_method to add methods.

method_missing(name, *args) is called when lookup fails. It can implement dynamic finders or proxies, but always pair it with respond_to_missing?, call super for names you don't handle, and consider defining real methods on first use instead, which is faster and plays well with introspection.

Hooks let classes react to events: inherited(subclass), included(base), method_added(name).

instance_eval and class_eval run a block with a different self; class_eval with a string compiles source code, which is fastest but needs care with untrusted input. Use these tools sparingly: metaprogrammed code is harder to grep, debug and type-check.

Example

class Model
  def self.flags(*names)
    names.each do |n|
      define_method("#{n}?") { @flags.include?(n) }
      define_method("#{n}!") { @flags << n; self }
    end
  end
  def initialize = @flags = []
end
class User < Model
  flags :admin, :verified
end
u = User.new.admin!
p [u.admin?, u.verified?]                 # [true, false]
p u.public_send(:verified?)               # false
class Settings
  def initialize(h) = @h = h
  def method_missing(name, *args)
    @h.key?(name) ? @h[name] : super
  end
  def respond_to_missing?(name, priv = false) = @h.key?(name) || super
end
s = Settings.new(theme: "dark")
p s.theme, s.respond_to?(:theme)          # "dark", true
begin; s.font; rescue NoMethodError; puts "no font"; end   # no font

Remember

  • define_method plus closures generate methods from data
  • Prefer public_send to send so private methods stay private
  • Pair method_missing with respond_to_missing? and call super

Watch out for

  • Implementing method_missing without calling super, which silently swallows typos.

Check yourself

  1. Why should method_missing call super for names it doesn't handle?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.ruby-lang.org/en/3.4/Module.html#method-i-define_method