Lesson 07 / 09
Dynamic Method Definition
Measuring how define_method produces methods at run time, how those methods check their parameters, and how the production classifies the author.
Published Updated
In this lesson
- Defines a method from a block with define_method
- Measures with the backtrace label where a generated method differs from one written with def
- Shows that the method_added hook fires the same way for def, define_method and attr_reader
- Classifies the author of a method generated from a string with class_eval
Contents
The previous lesson covered reflection: asking an object about its methods, state and type. Struct.new wrote the Point#x method, and the author helper called it code. There the production was a black box, and we looked only at the result. This lesson does the production itself. Dynamic method definition is defining a method not by writing def but by giving a name and a body while the program runs. In Ruby the tool for it is define_method. M11/K01 measured at which stage a decision is made in C; here the question is not when the definition comes into being. The question is again who wrote it.
The axis is the same: who wrote this method? The author in this lesson is code. The programmer writes a loop once, and the loop writes two methods. What leaves the programmer’s hands is only the define_method call, not the individual method bodies. To recall the four authors: the programmer writes with def, an ancestor is a superclass or a module, code is a production call, and nobody means there is no definition under that name. Dynamic definition is one of the paths of code. The reason it is needed is plain too: if dozens of methods follow the same pattern, writing the pattern once and taking the names from a list is shorter than writing each method, and when a name is added to the list the method is born by itself. This lesson measures the price of that brevity: a method name that is searched for in the source may never appear in the source. The helper below is the same as in the previous lesson and is given as it is.
# author.rb — a helper that classifies who wrote a method
# Classification asks in order: is there a definition in the receiver's chain at all,
# then does the receiver's own class carry it, then how was the definition produced.
def author(receiver, name)
chain = receiver.singleton_class
unless chain.method_defined?(name) || chain.private_method_defined?(name)
return ["nobody", "no definition"]
end
definition = chain.instance_method(name)
owner = definition.owner
place = owner == chain ? "singleton class" : owner.to_s
return ["ancestor", place] unless owner == receiver.class || owner == chain
iseq = RubyVM::InstructionSequence.of(definition)
return ["code", "#{place}, no ruby body"] if iseq.nil?
return ["code", "#{place}, block body"] if iseq.label.start_with?("block")
["programmer", "#{place}, def body"]
end
This block is source; the file is written to disk and pulled in with require_relative.
The Measured Classes
The observation of this lesson rests on two classes. Account provides a method written with def for comparison. Settings takes its methods from a loop. The excerpt of the shared definitions is as follows.
# authors.rb — the definitions needed for this lesson's observation
require_relative "author"
module Reportable
def report = "report: #{balance_text}"
end
class Account
include Reportable
def initialize(owner, balance)
@owner = owner
@balance = balance
end
def balance_text = "#{@owner} #{@balance}"
def net = @balance - fee
private
def fee = 2
end
class Settings
%i[color width].each do |name|
define_method(name) { "#{name} default" }
end
end
This block is source too. In the Settings definition, %i[color width] builds an array of two Symbol values; each calls define_method for every name and makes the block the method of that name. In the Settings class there is no def named color or width.
Who Wrote the Generated Method
The observation of this lesson is the color method of a Settings object. The file below calls the helper and prints the value the method produces next to it.
# generated.rb — who wrote Settings#color require_relative "authors" s = Settings.new p author(s, :color) puts s.color
["code", "Settings, block body"] color default
The author is code. The owner of the method turned out to be Settings itself, so it is not an ancestor; but the body is not a def, it is a block given to define_method. The helper read this distinction from the backtrace label starting with block. Point#x in the previous lesson was code too, but it had no body at all: the answer there was no ruby body, and here it is block body. Two different forms fall into the same category. MP10. Only the color method was measured; width comes from the same loop and will give the same result, but that is an assumption.
The Trace of Production
The body of a method object has a backtrace label that Ruby writes into the compiled body. The helper looks at the label and asks whether it starts with block. We print the label directly to compare two methods: color, produced by the loop, and net, written with def.
# label.rb — the body label of two methods require_relative "authors" p Settings.instance_methods(false) iseq = RubyVM::InstructionSequence puts iseq.of(Settings.instance_method(:color)).label puts iseq.of(Account.instance_method(:net)).label
[:color, :width] block (2 levels) in <class:Settings> net
instance_methods(false) listed only the public methods of Settings itself. The labels show the difference: block (2 levels) in <class:Settings> for color, and net for net. The two levels say that the definition sits inside two blocks: one is the each block, the other is the define_method block. The label of net is the name of the method itself. The start_with?("block") question of the helper separates these two labels. MP11. The label format was measured in Ruby 4.0.7; the text may change in another version, but starting with block is the only thing drawn from this measurement.
Adding a Method While Running
define_method takes a name and a block, binds the method to that name and returns the name as a Symbol. The method can be called right after that line. It works when the class is already loaded: adding a new method to a class does not require reopening it with the class keyword.
# define.rb — adding and removing a method with define_method
require_relative "authors"
result = Settings.define_method(:theme) { "dark" }
p result
p Settings.new.theme
p author(Settings.new, :theme)
Settings.remove_method(:theme)
begin
Settings.new.theme
rescue NoMethodError => e
puts e.class
end
:theme "dark" ["code", "Settings, block body"] NoMethodError
The return value was :theme; the new method answered at once, and author classified it as code, just like those produced by the loop. The time of production changed and the form of the definition did not: the body is again a block. remove_method deleted the method from the own table of Settings, and the same message then raised NoMethodError. If an ancestor had a definition under the same name, that ancestor definition would become visible again; remove_method deletes only the class’s own definition. MP12. remove_method was measured only in this class and with a name that has no ancestor definition; the case with an ancestor definition is not shown here.
Deciding the Name While Running
The name of a method written with def is fixed in the source. define_method takes the name as a string or a Symbol; the name can come from a variable or a computation. The class below produces one reader and one writer for each of two field names, and it never carries the method names it produces as plain text in its source.
# form.rb — methods with computed names
require_relative "authors"
class Form
%w[first last].each do |field|
define_method("set_#{field}") { |value| instance_variable_set("@#{field}", value) }
define_method("get_#{field}") { instance_variable_get("@#{field}") }
end
end
f = Form.new
f.set_first("amy")
p f.get_first
p f.get_last
p f.instance_variables
p Form.instance_methods(false).sort
p author(f, :set_first)
"amy" nil [:@first] [:get_first, :get_last, :set_first, :set_last] ["code", "Form, block body"]
Four methods came out of a single loop: get_first, get_last, set_first and set_last. When the written field was read, "amy" came back; the field that was never written gave nil, because that instance variable was never assigned. instance_variables listed only @first: the instance variable of the second field does not exist until a first value is written to it. The author again came out code. Here the programmer wrote only a pattern; code produced the four instances of the pattern. MP13. This class was measured with two fields; the case where the name string contains characters that are invalid in a method name was not tested.
Parameter Checking
A block normally behaves loosely: a missing argument becomes nil. The block given to define_method behaves like a method and checks the number of arguments. The measurement below tests this in two ways: giving the block directly, and passing a proc object with &.
# parameters.rb — define_method blocks and argument checking
class Settings
define_method(:double) { |n| n * 2 }
define_method(:pdouble, &proc { |n| n * 2 })
end
s = Settings.new
p Settings.instance_method(:double).arity
p s.double(4)
p proc { |n| n }.call
[:double, :pdouble].each do |name|
begin
s.public_send(name)
rescue ArgumentError => e
puts "#{name} -> #{e.class}"
end
end
1 8 nil double -> ArgumentError pdouble -> ArgumentError
The arity came out 1 for double: the generated method carries the parameter of the block as a required parameter. double(4) gave 8. The third line shows the contrast: an ordinary proc called without an argument raised no error and returned nil. The block of the same form inside define_method raised ArgumentError; passing the proc object with & did not change this, and pdouble raised the error too. So at the moment of production the block moves to the rules of a method. MP14. Only blocks with a single required parameter were measured; blocks with default values and variable arguments were not tested in this lesson.
Variables the Block Sees
The most useful difference between define_method and def is that the block sees the local variables around it. A closure is a block carrying the local variables of the place where it was defined, even when it is called later. def opens a new scope and does not see the local variables around it; a block does.
# counter.rb — a define_method block sees the variable around it
class Counter
total = 0
define_method(:bump) { total += 1 }
def peek = defined?(total).inspect
end
a = Counter.new
b = Counter.new
p [a.bump, a.bump, b.bump]
puts a.peek
[1, 2, 3] nil
Three calls gave [1, 2, 3]. total was a local variable in the class body, and the bump block carried it as a closure. The counter was shared between the two objects: the first call on the second object gave 3, because the variable is not in the object, it is in the closure of the block. peek, written with def, could not see the same name; defined?(total) returned nil there, and inspect printed it as "nil". For this reason the state of a method produced with define_method may not be in instance variables; the instance_variables call of reflection does not show this counter. MP15. The closure of the block was measured with only one local variable in this class body; sharing the closure among other methods is not shown in this example.
The Hook
When a new method is added to a class, Ruby calls the method_added method of the class. A hook is a method defined to run when a certain event happens. The class below writes this hook and collects the added names in a list. Methods are added in three different ways: def, define_method and attr_reader.
# hook.rb — in which ways method_added fires
class Jar
@added = []
class << self
attr_reader :added
def method_added(name)
@added << name
end
end
def a = 1
define_method(:b) { 2 }
attr_reader :c
end
p Jar.added
[:a, :b, :c]
All three ways fired the hook the same way: [:a, :b, :c]. The hook does not know by which way the method was produced; it receives only the name. This must be read together with the observations of the earlier lessons: the firing of the hook does not tell the author. What tells the author is the form of the body of the definition, and the hook does not see it. So the hook is good for watching where methods are added, but it does not answer the question of who wrote it. MP16. The hook was measured only in this class and while the class body was running.
The Accessor Also Produces Code
attr_accessor produces a reader and a writer method with the same name. Accessor is the joint name of these two methods; only the reader is called attr_reader, and only the writer attr_writer.
# access.rb — attr_accessor produces two methods require_relative "author" class Access attr_accessor :color end p Access.instance_methods(false).sort p author(Access.new, :color) p author(Access.new, :color=)
[:color, :color=] ["code", "Access, no ruby body"] ["code", "Access, no ruby body"]
Two methods came out: color and color=. The author of both is code, but the form of the body differs from define_method: there is no label, because attr_accessor leaves no Ruby body. No def was written in the Access class. block body and no ruby body are the two sub-forms of code. MP17. Only attr_accessor was measured; attr_writer alone was not measured.
The Bounding Measurement
So far code has meant a define_method or attr_* call. There is one more form of production: making a class interpret a string as Ruby code. class_eval runs a string in the context of the class. The measurement below does the same job in two ways: one with a string, the other with a block.
# limit.rb — the same job, two ways of production
require_relative "authors"
Settings.class_eval "def from_string = 4"
Settings.define_method(:from_block) { 4 }
s = Settings.new
p [s.from_string, s.from_block]
p author(s, :from_string)
p author(s, :from_block)
[4, 4] ["programmer", "Settings, def body"] ["code", "Settings, block body"]
Both methods gave 4, but their authors split. The method produced from the string was created at run time, yet author classified it as programmer, because there is a def body inside the string. The one produced with define_method is code. The rule reads the form of a definition, not its intent: the category measures the form in which the definition is written, not when the production happened. So production by code does not always fall to code. The reverse holds too: seeing define_method in the source does not say the author is code, and a def written inside a string falls to the programmer even though it was produced at run time. Reflection sees the owner of the method and the form of its body; it does not see the call that started the production. Tracing the production is therefore less a matter of looking at the label than of searching the source for define_method and class_eval calls. MP18. Production from a string was measured only with a simple one-line def; in more complex strings the label and the body form may differ.
Summary
define_methodtakes a name and a block, binds the method to that name and returns the name as aSymbol; the new method can be called at once.- The author of a generated method is code: its body is a block, and its backtrace label starts with
block. The label of a method written withdefis the name of the method. - The block of
define_methodchecks the number of arguments like a method; an ordinaryprocdoes not. - The
method_addedhook fires the same fordef,define_methodandattr_reader; the hook does not tell the author. attr_accessorproduces two methods and the author of both is code, but the form of the body is different.- A
defproduced from a string withclass_evalcomes out programmer: the category measures the form of the definition, not the moment of production.
Next Step
In this lesson we defined the methods ourselves, before they were called. The next lesson measures the opposite: what Ruby does when no definition exists for the called name, and how a class can fill that gap itself. In that lesson the fourth category of the author, nobody, appears.
To keep your progress and take notes, Log in
My notes
Log in to take notes.