Skip to content
Lesson 1 of 7

Step 1 of 5 · Reading · ~2 min

Learn

Metaprogramming

define_method and Dynamic Methods

define_method lets you create methods at runtime. Rails leans on it heavily — has_many, validates and friends all generate methods you never typed.

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

  def initialize
    @config = {}
  end
end

c = Config.new
c.host = "localhost"
c.port = 5432
puts c.host    #=> localhost

That is 8 methods (4 readers + 4 writers) from four lines. Note where the each loop sits: directly in the class body, not inside a method.

The receiver decides — and this is where people get stuck

define_method is a private instance method of Module. The implicit receiver has to be a class or module, which is true in a class body and false everywhere else. Move that same loop into initialize and Ruby stops you:

class Box
  def initialize(attrs)
    attrs.each { |a| define_method(a) { } }   # self is a Box, not a Class
  end
end

Box.new([:width])
#=> undefined method `define_method' for #<Box:0x000055...> (NoMethodError)
#=> Did you mean?  define_singleton_method

Ruby's own hint is the fix. Inside an instance method you have two honest choices:

  • define_singleton_method(name) { ... } — defines the method on this one object, via its singleton class. This is what you want when each object gets a different set of methods.
  • self.class.send(:define_method, name) { ... } — defines it on the class, for every instance. send is needed because define_method is private.

Reading and writing ivars by name

A generated accessor cannot write @width literally — the name only exists as a variable. Use the string form, and remember the @ is part of the name you pass:

user = Object.new
user.instance_variable_set(:@name, "Linus")
puts user.instance_variable_get(:@name)   #=> Linus
puts user.instance_variable_get("@name")  #=> Linus (a String name works too)
# instance_variable_get("name")           #=> NameError - the @ is not optional

Calling methods by name

class User
  attr_accessor :name, :email
end

user = User.new
fields = { name: "Ada", email: "[email protected]" }
fields.each { |k, v| user.send("#{k}=", v) }

User.method_defined?(:name)   #=> true — does the class define this instance method?
user.respond_to?(:name)       #=> true — can this object answer it?

send calls a method by name and ignores private/protected. public_send is the same call with the access check left on; reach for it whenever the name came from outside your code.

Your exercise

You will build a Box whose constructor is handed a list of attribute names and generates an accessor pair for each. The mistake the grader catches most often is exactly the one above: calling define_method from inside initialize and getting NoMethodError. The second most common is copying the @config[attr] body out of the Config example above — Box has no @config hash, so it comes back undefined method '[]=' for nil:NilClass. Store each value in its own instance variable instead.

Up nextmethod_missingMetaprogramming

Discussion

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

Sign in to post a comment or reply.

Loading…