Skip to content

define_method turns a block into a method

Level: 301 · for a reader who has generated a few attr_accessors by hand and wants to generate methods in a loop

One line: define_method(:name) { ... } installs a method whose body is a closure — it sees the local variables of the class body, each block call keeps its own copy of a loop variable, and the call returns the method's name as a Symbol — whereas Python's setattr(Cls, name, func) installs any callable and leaves the closure problem, the late-binding loop variable, for you to solve.

def opens a new scope: nothing outside it is visible inside, which is why a method generated with def cannot use a value computed at definition time. define_method is the alternative: it takes a Symbol and a block (or an existing Method, UnboundMethod or Proc), and the block is a closure, so a loop that calls define_method three times with each gives each method its own iteration variable. The block becomes a method with lambda rules — a strict argument count — and it cannot yield, because there is no block-of-the-block to yield to; it declares &blk instead. define_method returns the Symbol it was given, which is what makes private define_method(:x) { } a one-liner. It is a public method, so Klass.define_method(...) works from outside the class too (row 3) — since Ruby 2.5 (the version is not machine-checked here).

Verified output of define_method_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

 1. for c in [...]: for_red? for_green? for_blue?  [false, false, false]
 2. each { |c| }: red? green? blue?                [true, false, false]
 3. define_method(:noop) { } returned              :noop
    Light.define_method(...) from outside the class :out
 4. hello (from instance_method(:greet))           "hi"
    double (from a lambda): double(4)              8
 5. double with no argument                        ArgumentError: wrong number of arguments (given 0, expected 1)
 6. triple(3) -- the block sees factor             9
    triple_def(3) -- def does not                  NameError: undefined local variable or method 'factor' for an instance of Light
 7. define_method(:m) { yield }                    SyntaxError: Invalid yield
    takes_block { |x| x * 10 } -- use &blk         20
 8. private define_method(:secret): light.secret   NoMethodError: private method 'secret' called for an instance of Light
    light.send(:secret)                            :s
 9. my_attr :name, :age -> name, age               "Ada", 36
    Person.instance_methods(false).sort            [:age, :age=, :name, :name=]
10. class_eval "def str_#{c}? ..." per colour      [true, false, false]
11. Light.instance_methods(false).grep(/red/)      [:for_red?, :is_red?, :red?, :str_red?]
12. is_red? -- is?(:red) behind a closure          true

Reading the output

Row 1 is the trap and row 2 the cure. A for loop does not create a scope, so all three blocks share one variable c, which ends the loop holding :blue; a red light then answers false to every predicate. each calls its block once per element with a fresh c, and red? is true. Row 3: the return value is the Symbol, which row 8 uses — private accepts a Symbol, so private define_method(:secret) { } defines and hides in one expression, and send still reaches it. Row 4: the second argument may be a callable instead of a block; instance_method(:greet) gives a plain alias, and a lambda becomes the body. Row 5: the generated method has lambda arity — calling double with no argument is an ArgumentError, exactly as for a def. Row 6 is the reason the method exists: the block reads factor from the class body, while triple_def, written with def, raises NameError because def sees nothing outside itself. Row 7: yield inside a define_method block is rejected by the parser as "Invalid yield" — a block has no block of its own — so a generated method that wants a block declares &blk. Row 9 rebuilds attr_accessor in four lines, and instance_methods(false) lists the four generated readers and writers. Row 10 is the older route: class_eval with a string. It works, but the string cannot close over anything — the colour is interpolated into the source as text — and it is compiled at run time. Row 11 shows the generated methods are ordinary members of the class, and row 12 shows the idiom for specialising one general method: a closure that fixes an argument.

Compared with Python

Python installs a method by assigning a function to a class attribute, so setattr(Cls, name, func) in a loop is the define_method loop. Row 1 is the same trap with the opposite default: a Python for loop, like Ruby's, does not make a scope, and a lambda reads its free variable when called — late binding — so all three read the last c. The cure is not a different loop but a default argument, lambda self, c=c:, which evaluates c at definition time (row 2); a factory function, as in row 6, does the same job. Row 3: setattr is a statement in spirit and returns None, so there is no private setattr(...) one-liner — and there is no private either (row 8): setattr(Light, "__secret", f) stores the name exactly as written, because name mangling happens when the compiler sees __secret inside a class body, never at run time, so hasattr(light, "_Light__secret") is False. Row 5: a wrong argument count is a TypeError in Python; there is no lenient proc form to contrast with. Rows 6 and 7 are non-events in Python: a def is a closure already, and a callback is simply a parameter, so nothing corresponds to "Invalid yield". Row 9 rebuilds the accessor pair with property, again with the n=n default to freeze the loop variable, and row 12 shows functools.partialmethod, the standard-library spelling of "one general method, specialised" that Ruby writes as a closure. The one idea with no Python twin is row 8's visibility; the one idea with no Ruby twin is the c=c default — Ruby never needs it because a block parameter is fresh per call.

Verified output of define_method_py.py — regenerated by tools/run_examples.py, never hand-typed.

 1. for c in ...: for_red for_green for_blue       [False, False, False]
 2. lambda self, c=c: is_red is_green is_blue      [True, False, False]
 3. setattr(Light, 'noop', ...) returned           None
    setattr is always called from outside          -
 4. hello (Light.hello = Light.greet)              'hi'
    double (from a lambda): double(4)              8
 5. double with no argument                        TypeError
 6. triple(3) -- the closure sees factor           9
    a def is a closure too                         no NameError to show
 7. no yield to fail: a callback is a parameter    -
    each_twice(lambda x: x * 10)                   [10, 20]
 8. setattr(Light, "__secret", ...) then getattr   's'
    hasattr(light, "_Light__secret")               False
 9. my_attr(Person, "name", "age") -> name, age    'Ada', 36
    sorted(vars(Person)) minus dunders             ['age', 'name']
10. exec(f'def str_{c}(self): ...') per colour     [True, False, False]
11. sorted(k for k in vars(Light) if 'red' in k)   ['for_red', 'is_red', 'is_red2', 'str_red']
12. is_red2 -- partialmethod(is_colour, 'red')     True

The late-binding rule and the i=i fix are measured on their own page in chapter 03: closures capture variables.

Try it

  1. Build a Length class that generates to_cm, to_mm and to_km from a FACTORS hash with define_method, so that each method keeps its own factor; add one private conversion with private define_method(...) and show that respond_to? says false for it while send still reaches it.
  2. Reimplement attr_reader as counted_attr_reader, whose generated readers count how many times each has been called (keep the counts in a Hash held by the closure), and print the counts after a few reads. What happens to the count if you dup the object?
Solution to 1

define_method_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# define_method_kata_rb.rb — unit conversions generated from a table.

class Length
  FACTORS = { cm: 100, mm: 1000, km: 0.001 }

  def initialize(metres) = @metres = metres

  FACTORS.each do |unit, factor|                       # each block call has its own factor
    define_method(:"to_#{unit}") { (@metres * factor).round(4) }
  end

  private define_method(:to_leagues) { (@metres / 4828.032).round(4) }
end

len = Length.new(1.5)
puts "1. Length.new(1.5).to_cm             -> #{len.to_cm}"
puts "2. Length.new(1.5).to_mm             -> #{len.to_mm}"
puts "3. Length.new(1.5).to_km             -> #{len.to_km}"
puts "4. instance_methods(false).sort       -> #{Length.instance_methods(false).sort.inspect}"
puts "5. respond_to?(:to_leagues)           -> #{len.respond_to?(:to_leagues)} (private)"
puts "6. send(:to_leagues)                  -> #{len.send(:to_leagues)}"

Verified output of define_method_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

1. Length.new(1.5).to_cm             -> 150.0
2. Length.new(1.5).to_mm             -> 1500.0
3. Length.new(1.5).to_km             -> 0.0015
4. instance_methods(false).sort       -> [:to_cm, :to_km, :to_mm]
5. respond_to?(:to_leagues)           -> false (private)
6. send(:to_leagues)                  -> 0.0003

See also