Skip to content
send, define_method, and method_missing
step 1/7

Reading — step 1 of 7

Learn

~2 min readMetaprogramming

Ruby is famously dynamic. The Pickaxe spends entire chapters on metaprogramming — code that writes code at runtime. Three pillars: send, define_method, and method_missing.

send — call a method by name

class User
  def initialize(name); @name = name; end
  def name; @name; end
end

u = User.new("Ada")
u.send(:name)             # => "Ada" — same as u.name
u.send("name")            # also works (string or symbol)
u.send(:name=, "Linus")   # call setter dynamically

send even calls private methods — useful for testing internals, dangerous for security. Use public_send if you want to respect visibility.

Useful for dispatching by user input or config:

actions = { 'create' => :create_user, 'delete' => :destroy_user }
self.send(actions[input])

define_method — create methods at runtime

class Config
  [:host, :port, :user, :password].each do |attr|
    define_method(attr) { @data[attr] }
    define_method("#{attr}=") { |v| @data[attr] = v }
  end
end

This is how attr_accessor is implemented (roughly). Define many similar methods without repeating yourself.

Key trick: define_method takes a BLOCK, which is a closure — the block captures variables from the enclosing scope. Compare to def, which doesn't.

method_missing — handle unknown method calls

class DynamicAttrs
  def initialize
    @data = {}
  end
  
  def method_missing(name, *args)
    name_str = name.to_s
    if name_str.end_with?('=')
      @data[name_str.chomp('=').to_sym] = args.first
    else
      @data[name]
    end
  end
  
  def respond_to_missing?(name, include_private = false)
    true
  end
end

d = DynamicAttrs.new
d.foo = 42
d.foo            # => 42 — went through method_missing

Always implement respond_to_missing? to match — otherwise respond_to?(:foo) returns false, breaking duck-typing checks throughout the standard library.

Reflection methods

User.instance_methods(false)        # methods defined ON User (not inherited)
u.methods.grep(/age/)               # all methods matching the pattern
User.method_defined?(:name)
u.respond_to?(:name)
u.method(:name).source_location     # [file, line] where it was defined

At runtime, you can introspect anything.

When to reach for metaprogramming

Good fits:

  • DSLs (RSpec, ActiveRecord, Rails routes)
  • Reducing repetition for parallel methods (like attr_accessor)
  • Adapters that wrap dynamic APIs

Avoid for:

  • Anything you could write with normal methods + a hash lookup
  • Anything performance-critical (method_missing is slow)
  • Any code where readers will be confused

Metaprogramming is the single biggest source of "I can't find where this method is defined" headaches. Use sparingly.

Common mistakes

  • Implementing method_missing without respond_to_missing? — silently breaks duck typing.
  • Overusing send — bypasses encapsulation, harder to refactor.
  • define_method inside def — wrong scope for the block. Use it at class scope.
  • Catching too much in method_missing — missing methods (typos, etc.) get swallowed silently. Always check the name and call super for unknowns.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…