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
grepcan't find a method's definition, document where it's generated.