Skip to content
academia.sh

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_method takes a name and a block, binds the method to that name and returns the name as a Symbol; 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 with def is the name of the method.
  • The block of define_method checks the number of arguments like a method; an ordinary proc does not.
  • The method_added hook fires the same for def, define_method and attr_reader; the hook does not tell the author.
  • attr_accessor produces two methods and the author of both is code, but the form of the body is different.
  • A def produced from a string with class_eval comes 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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close