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¶
- Build a
Lengthclass that generatesto_cm,to_mmandto_kmfrom aFACTORShash withdefine_method, so that each method keeps its own factor; add one private conversion withprivate define_method(...)and show thatrespond_to?saysfalsefor it whilesendstill reaches it. - Reimplement
attr_readerascounted_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 youdupthe 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.
See also¶
sendreaches private methods;public_senddoes not — calling the method whose name you generated- Hooks fire when code is defined, not when it runs —
method_addedsees adefine_methodexactly as it sees adef instance_evalmovesself,class_evalmovesdef— the string route of row 10, and where adefinside an eval lands- Closures capture variables — why
eachgives each block its owncand why a Python lambda needsc=c - Classes are open —
define_methodon a class that already exists - Singleton classes —
define_singleton_method, the one-object version forloops do not scope — the reason row 1 fails in both languages- Instance variables are private — An
@variablebelongs to one object and can be read or written only by that object's own methods method_missingneedsrespond_to_missing?—method_missing(name, *args)runs only after normal lookup has failed, and it must callsuperfor names…