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.sendis needed becausedefine_methodis 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.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…