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.senddoes the same but also reaches private methods, so preferpublic_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 howattr_accessor-style macros are built.instance_variable_get(:@x)andinstance_variable_setread and write state by name.respond_to?(:name)andmethod_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 fontRemember
- 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.