Lesson 06 / 09
Reflection
Measuring how to ask an object about its own methods, state and type at run time, and how those questions classify who wrote a method.
Published Updated
In this lesson
- Asks an object about its methods, instance variables and type through reflection
- Reads the arity and parameters of a method from the method object
- Explains which questions the author helper is built from
- Measures why reflection answers only partially for methods that are not written in Ruby
Contents
The previous lesson showed that the same message can reach two different authors in two instances of the same class: the singleton class of a single object sits at the very front of the method lookup chain. The last sentence of that lesson left a question behind: can we ask the object itself, at run time and without opening the file, who wrote the x method of Point? This lesson answers it. Reflection is a program examining its own objects, classes and methods while it runs. Instead of calling a method, the program asks a question about the method.
The subject of the second half of the course is metaprogramming: a program that writes programs, or that changes the structure of the program while it runs. Reflection is its read-only half. It only asks and changes nothing. The next two lessons move on to writing. The axis stays the same: who wrote this method? There are four possible authors: programmer, ancestor, code and nobody. The programmer wrote the method with def. An ancestor wrote it as a superclass or a module. Code produced it: a piece of code generated the method, and no body was written by hand. Nobody means there is no definition under that name at all.
The Classifying Helper
The file below is the shared helper of the course, and this lesson gives it as it is. It asks three questions: is there a definition under that name in the receiver’s chain, does the receiver’s own class carry the definition, and does the definition rest on a def body. Each question is a reflection call, and we will look at them one by one.
# 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 and has no output; it is only written to disk, and the other files pull it in with require_relative. The helper reads only the form: it looks at where the definition sits and what kind of body it has. It does not read the intent of the author. MP1. This classification is meaningful only for user classes written in Ruby; the result for core classes is measured at the end of this lesson.
The Measured Classes
The objects measured throughout the lesson are in the file below. It is an excerpt of the shared definitions of the course, with only what this lesson’s observation needs: an account class, its subclass, a module and a Struct.
# 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 TermDeposit < Account
end
Point = Struct.new(:x, :y)
Struct is a core class that produces a class with the given names. Each name becomes a member, and a reader and a writer method for that member come ready-made. Point is a class produced this way. This block is source too.
Who Wrote the Member Reader
The observation of the course in this lesson is the x method of a Point object. The file below calls the helper and prints the value the method produces next to it.
# member_reader.rb — who wrote Point#x require_relative "authors" pt = Point.new(3, 4) p author(pt, :x) puts pt.x
["code", "Point, no ruby body"] 3
The author is code: neither the programmer wrote x with def nor a superclass did; the Struct.new call created it while the class was being produced. The helper concluded this from two facts. The owner of the method turned out to be Point itself, so it is not an ancestor. And there was no Ruby body, because RubyVM::InstructionSequence.of returned nil for this method. Which question gives which answer is measured one by one in the next sections. MP2. One Struct class was measured in Ruby 4.0.7; this measurement cannot show whether another version of Struct writes its members differently.
Asking the Class and the Object
The first group of reflection asks about the structure of a class and an object: which members exist, who the superclass is, what the object holds as its own state.
# query.rb — asking a Point object through reflection require_relative "authors" pt = Point.new(3, 4) p Point.members p pt.to_a p Point.superclass p author(pt, :to_a) p author(pt, :x=) p pt.instance_variables
[:x, :y] [3, 4] Struct ["ancestor", "Struct"] ["code", "Point, no ruby body"] []
members returned the member names in order, and to_a returned the values in the same order. superclass said that Point descends from Struct. The two author calls gave two different results: Struct wrote the to_a method, because the owner is not Point, so the author is ancestor. Point itself carries the x= method and it has no body, so the author is code. Two methods of the same object reached two different authors. The last line is surprising: instance_variables came out empty. Struct keeps its values in its own internal layout and not in instance variables, so reflection finds no @x. MP3. The empty list was measured only for Struct; the instance variables of an ordinary class are seen in the next block.
Asking the State and the Private Method
In an object of an ordinary class, state lives in instance variables. Reflection lists them by name and reads their values by name. Private methods become visible when respond_to? is asked with a second argument of true.
# state.rb — asking an object for its state and its private method
require_relative "authors"
acct = Account.new("amy", 100)
p acct.instance_variables
p acct.instance_variable_get(:@balance)
p acct.respond_to?(:fee)
p acct.respond_to?(:fee, true)
p acct.respond_to?(:net)
[:@owner, :@balance] 100 false true true
instance_variables returned the two instance variables in the order they were created. instance_variable_get(:@balance) read the value; this call works even if no reader method is defined from outside, because it reaches instance variables directly through reflection. respond_to?(:fee) returned false, because fee is private and the default question covers only public methods. When the second argument true was given, the same question returned true. net is public and returned true. So the default respond_to? question asks not whether the method exists but whether it is visible to the caller; with true as the second argument, private methods enter the scope. MP4. The private method question was measured only with respond_to?; other queries such as private_methods were not used in this lesson.
Asking the Method Itself
Reflection can also catch a method as an object. instance_method gives a method object from the class, and method gives one from the object. A method object can be asked for its argument count, its parameters and the place it was defined, and it can be called.
# method_query.rb — asking the method itself
require_relative "authors"
acct = Account.new("amy", 100)
p Account.instance_method(:net).arity
p Account.instance_method(:initialize).parameters
p acct.method(:net).call
p Account.instance_method(:net).source_location.nil?
p Point.instance_method(:x).source_location.nil?
p Object.const_get(:Account)
0 [[:req, :owner], [:req, :balance]] 98 false true Account
The arity came out as 0 for net: the method takes no argument. parameters listed the two required parameters of initialize with their names; :req means required. acct.method(:net).call caught the method and called it, and gave 98: 100 minus the 2 of the private fee. source_location gives the file and line of the method; since the path depends on the environment, only whether it is empty was printed here. It is not empty for Account#net, because that method was written with def; it is empty for Point#x, because Struct wrote it and there is no source line. This is the observable counterpart of the nil check in author. The last line finds a constant from its name: const_get(:Account) returned the class itself. MP5. source_location was measured for only two methods; that it is not nil does not show by itself that the author is the programmer.
Asking the Owner
The heart of the author helper is a single question: who is the owner of the method object. The owner is the class or module where the definition of the method sits. To find the owner, the helper looks at the class chain of the receiver; we can list the same chain by hand as well.
# owner.rb — the owner of a method and the receiver's chain
require_relative "authors"
acct = Account.new("amy", 100)
td = TermDeposit.new("a", 1)
p TermDeposit.ancestors.first(3)
p acct.singleton_class.ancestors.drop(1).first(3)
p acct.method(:report).owner
p TermDeposit.instance_method(:net).owner
p author(td, :net)
p author(acct, :report)
[TermDeposit, Account, Reportable] [Account, Reportable, Object] Reportable Account ["ancestor", "Account"] ["ancestor", "Reportable"]
ancestors gives the ancestor chain of a class from left to right: first the class itself, then the modules it includes and its superclasses, in the order the method lookup reads them. For TermDeposit the first three were TermDeposit, Account and Reportable: a module stands right behind the class that includes it. The chain of the object’s own singleton class starts with the singleton class itself; that name changes in every run, so it is not printed, the first element is dropped, and the rest of the chain appears as Account, Reportable, Object. The owner of the report method came out as Reportable, while TermDeposit.instance_method(:net) reached the definition of Account, not of TermDeposit. For this reason author(td, :net) and author(acct, :report) both classified the author as ancestor: the owner is neither the class nor the singleton class of the receiver. MP6. The chain was printed with only its first three elements; the tail after Object was not measured.
The Method of a Single Object
Back to the observation of the previous lesson: the same message can reach two authors in two instances of the same class. Reflection asks this too. singleton_methods lists only the methods defined in the object’s own singleton class. The measurement below builds two Account objects and writes its own net method for only one of them.
# singleton.rb — the same message in two instances
require_relative "authors"
a = Account.new("amy", 100)
b = Account.new("tom", 50)
def b.net = 0
p a.singleton_methods
p b.singleton_methods
p a.net
p b.net
p author(a, :net)
p author(b, :net)
[] [:net] 98 0 ["programmer", "Account, def body"] ["programmer", "singleton class, def body"]
The list is empty for a, and net is in it for b. Two instances of the same class answered the same message with two different values: 98 for a and 0 for b. The two author calls name the difference. The def in the Account class wrote a.net; a def wrote b.net too, but this time the definition sits in the singleton class of b, and the helper calls it singleton class in place of an owner name. The author is the programmer in both cases; the difference is in the place of the definition. Reflection therefore separates two questions: who produced the method, and in which layer does it sit. MP7. Only the net method was measured in two instances; the relation of a singleton method to a class method was not established in this lesson.
Type and Method List
The last group asks what type an object is and what the list of a class’s own methods is. The list is taken once more after the class is reopened: an attr_reader is added to the reopened class and the list changes.
# kind.rb — asking an object's type and a class's methods
require_relative "authors"
acct = Account.new("amy", 100)
td = TermDeposit.new("a", 1)
p acct.is_a?(Reportable)
p td.is_a?(Account)
p td.instance_of?(Account)
class Account
attr_reader :owner
end
p Account.public_instance_methods(false).sort
p author(acct, :owner)
true true false [:balance_text, :net, :owner] ["code", "Account, no ruby body"]
is_a? returns true for every class and module in the chain: acct belongs to Reportable, and td belongs to Account. instance_of? accepts only the exact class; the Account question came out false for a TermDeposit instance. public_instance_methods(false) lists only the public methods of Account itself; false leaves out those that come from ancestors. For this reason report, which comes from Reportable, is not in the list, and neither is initialize, because initialize is private. attr_reader :owner added owner to the list. And author classified its author as code: attr_reader leaves no def body, it produces the method itself. MP8. The list was taken after the moment of reopening; the class is assumed not to be reopened anywhere else.
The Bounding Measurement
So far reflection has worked on classes the programmer wrote. What happens when the helper is turned to core classes?
# limit.rb — the limits of the rule
require_relative "authors"
p author("a", :upcase)
p author(Account, :new)
[5, :a].each do |value|
begin
value.singleton_class
rescue => e
puts "#{value.inspect} -> #{e.class}"
end
end
p nil.singleton_class
["code", "String, no ruby body"] ["code", "Class, no ruby body"] 5 -> TypeError :a -> TypeError NilClass
For String#upcase and Class#new the helper said code, but this answer misleads. The core of Ruby wrote these methods with compiled code; since there is no def body, the rule assigned them to code, for the same reason as Struct members. The difference between them is invisible to the helper: one is produced at run time, the other was written in the core from the start. The rule reads the form of a definition, not its intent. The second limit was harsher: an integer and a Symbol value have no singleton class, and both gave a TypeError. nil returned NilClass, so the helper works for nil. The limit that follows from these three results is this: reflection says correctly where a definition sits, but it does not always say who the author is. MP9. The two results for core classes were measured for only these two methods; other core methods may come out differently.
Summary
- Reflection is asking an object about its methods, state and type at run time;
members,instance_variables,respond_to?,is_a?andpublic_instance_methods(false)are such questions. - Code wrote
Point#x:Struct.newcreated it while the class was being produced, and there is nodefbody. - An ancestor wrote
Point#to_a: the owner isStruct. - A method object is queried with
arity,parametersandsource_location, and it is called withcall. - A reader added with
attr_readeralso counts as written by code. - The helper gives a meaningful result only for user classes written in Ruby; the two measured core methods (
String#upcase,Class#new) went to code.
Next Step
In this lesson, code produced the method of a class while the class was being defined, and we read the production only from its result. The next lesson does the production itself: define_method defines, with a block, a method whose name is decided at run time. The same question asks, this time, how code comes to be the author.
To keep your progress and take notes, Log in
My notes
Log in to take notes.