Skip to content
academia.sh

Lesson 08 / 09

Catching Missing Methods

Measuring what Ruby does when a message has no definition, how a class catches it with method_missing, and what reflection reports about the catch.

Published Updated

In this lesson

  • Catches a message that has no definition with method_missing and forwards it to a wrapped object
  • Explains why the author of a caught name is nobody while the catching method is written by the programmer
  • Tells reflection about a caught name with respond_to_missing?
  • Measures how a self-defining proxy turns nobody into code on the first call
Contents

The previous lesson measured a method defined at run time with define_method, and its author was code. There every name had been defined before it was called. This lesson asks the opposite: what does Ruby do when the called name has no definition at all? The default answer is NoMethodError. But a class can fill this gap itself. Catching missing methods (method_missing) is handing a message that was sent to an object, and found no definition in the method lookup chain, to the object’s own method_missing method before an error is raised. In Ruby the name of this method is code and is written as it is.

The axis is the same again: who wrote this method? The author in this lesson is nobody. There is no definition under the called name; the message still gets an answer, because another method catches it. For the author helper this is the last of the four categories, and the sections below show why it is used here. First the helper is given, in the same form as in the earlier lessons.

# 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 Class

The observation of this lesson is on a proxy object: an object that wraps another object and forwards the messages it receives to that object. Proxy defines none of the methods of the object it wraps.

# authors.rb — the definition needed for this lesson's observation
require_relative "author"

class Proxy
  def initialize(target)
    @target = target
  end

  def method_missing(name, *args, &block)
    return super unless @target.respond_to?(name)

    @target.public_send(name, *args, &block)
  end

  def respond_to_missing?(name, include_private = false) = @target.respond_to?(name, include_private) || super
end

This block is source too. method_missing receives three things: the name of the message, the arguments and, if there is one, the block. The first line hands the message to Ruby’s default behavior, which is NoMethodError, through super if the wrapped object does not understand it. If it does, the message is forwarded to it with the same arguments and the same block through public_send. respond_to_missing? is the second method; what it is for will be measured shortly.

Who Wrote the Undefined Method

The observation of this lesson is the sort method of a Proxy object. The object wraps an array; in the Proxy class there is no definition named sort.

# proxy.rb — who wrote Proxy#sort
require_relative "authors"

px = Proxy.new([3, 1, 2])
p author(px, :sort)
p px.sort
["nobody", "no definition"]
[1, 2, 3]

The author is nobody: the author helper found no definition for sort in the receiver’s chain and stopped at the first question. Still, px.sort worked and gave [1, 2, 3]. So a working method exists, but it is not in the method lookup chain. What makes it work is method_missing; it forwards the message to the array’s own sort method. This contradiction is the subject of the lesson: the author can be nobody and the answer still comes. MP19. This measurement is for a single message; the other messages of Proxy are measured separately below.

Caught Messages

The proxy answers not only sort but any message that has no definition in its chain and is a public method of the wrapped array. The measurement below tries three messages: one without arguments, one that returns a result of another type, and one that takes a block.

# catch.rb — the proxy catching messages
require_relative "authors"

px = Proxy.new([3, 1, 2])
p px.sort
p px.size
p px.map { |x| x * 10 }
[1, 2, 3]
3
[30, 10, 20]

sort gave the sorted array and size the length. map took a block: method_missing caught the block with &block and passed it on to public_send, so the array multiplied every element by ten. The proxy answered without defining any of these three messages. MP20. This measurement covers only three messages; array methods that have no definition in the chain pass the same way, but more than these three were not seen here, and names that do have a definition in the chain are measured separately below.

Arguments and Block

The three messages showed half of the signature of method_missing. Passing arguments and the block together must be measured separately, because if *args and &block are not written, the parts of the message silently drop.

# forward.rb — forwarding arguments and the block
require_relative "authors"

px = Proxy.new([3, 1, 2])
p px.first(2)
p px.inject(10) { |total, x| total + x }
p px.each_slice(2).to_a
[3, 1]
16
[[3, 1], [2]]

first(2) carried an argument, inject(10) { ... } carried both an argument and a block, and each_slice(2) returned an enumerator without a block, which to_a then unrolled. All three gave the same result as the array’s own call: the proxy carried the message without breaking it. The carrying is the work of the public_send(name, *args, &block) line; had *args or &block been missing from that line, the argument or the block would not have reached the message in these calls and the result or the error would have changed, but this was not tried in the lesson. MP21. Three calls were measured; keyword arguments were not tried in this lesson.

What Reflection Sees

The reflection lesson gave the ways of asking an object about its methods. Below, the proxy is asked four questions: respond_to?, defined?, method(:sort).owner and the class’s method_defined? question.

# query.rb — asking the proxy through reflection
require_relative "authors"

px = Proxy.new([3, 1, 2])
p px.respond_to?(:sort)
p defined?(px.sort)
p px.method(:sort).owner
p Proxy.method_defined?(:sort)
p author(px, :size)
true
"method"
Proxy
false
["nobody", "no definition"]

The answers look inconsistent, and the reason is respond_to_missing?. Since this method was defined when the proxy was written, respond_to? gave true, defined? returned "method" and method(:sort) produced a method object. The owner of the produced object came out Proxy, but when the definition table of the class was asked, Proxy.method_defined?(:sort) gave false. So respond_to_missing? tells reflection about the catch, but it adds no entry to the definition table. Because the helper looks at the definition table, it said nobody for size as well. Reflection looks at a method in two ways: is the message answered, and is there a definition. These two questions part ways in the proxy. MP22. These answers were given because respond_to_missing? is defined by the proxy; the result without it is measured in the section Telling Reflection.

The Proxy Is Not an Array

The proxy wraps an array, but it is not an array. Assuming that an object that behaves like an array is an array is dangerous; the measurement below draws the limits of this.

# identity.rb — the proxy and identity
require_relative "authors"

px = Proxy.new([3, 1, 2])
begin
  px.sortt
rescue NoMethodError => e
  puts e.class
end
p px.is_a?(Array)
p px == [3, 1, 2]
p px.class
p author(px, :class)
NoMethodError
false
false
Proxy
["ancestor", "Kernel"]

The misspelled sortt message raised NoMethodError: method_missing saw that the wrapped array did not understand the message and left it to Ruby’s default through super. is_a?(Array) came out false; the proxy is not an array, it is an object that holds an array. px == [3, 1, 2] gave false too, because == is a method that has a definition in the chain and the proxy does not catch it: catching works only for names that are not found in the chain. The class message was not caught for the same reason and returned Proxy; in Ruby 4.0.7 its author is ancestor, Kernel. The messages the proxy catches are those that have no definition in the chain at all. MP23. Only ==, is_a? and class were measured in this section; to_s is tested in a separate section.

Names in the Chain Are Not Caught

The section The Proxy Is Not an Array showed that catching does not work for == and class. The same rule should hold for other names that every object has, such as to_s. It is compared with to_a, which is not in the chain.

# in_chain.rb — comparing to_s and to_a
require_relative "authors"

px = Proxy.new([3, 1, 2])
p px.to_a
p px.to_s == [3, 1, 2].to_s
p author(px, :to_s)
p author(px, :to_a)
[3, 1, 2]
false
["ancestor", "Kernel"]
["nobody", "no definition"]

to_a was caught because it is not in the chain, and it gave the array. to_s was not caught because in Ruby 4.0.7 it comes from Kernel: the text of the proxy is not the text of the array, and its author is ancestor. The two names are equally ordinary for an array, yet for the proxy one came out nobody and the other ancestor. This difference draws the limit of the proxy: if you want to copy the behavior of the wrapped object, the names that every object already has must be handled separately. MP24. The measurement is for to_s and to_a; other ready-made names such as inspect and hash follow the same rule, but they were not measured.

The Catcher’s Own Author

So far we have looked at the name that is caught. The same question can be asked of the catching method itself: whose is method_missing?

# catcher.rb — the authors of method_missing and respond_to_missing?
require_relative "authors"

px = Proxy.new([3, 1, 2])
p author(px, :method_missing)
p author(px, :respond_to_missing?)
p author(Object.new, :method_missing)
p BasicObject.private_method_defined?(:method_missing)
["programmer", "Proxy, def body"]
["programmer", "Proxy, def body"]
["ancestor", "BasicObject"]
true

The caught sort came out nobody; the catching method_missing is the programmer: it was written with def inside Proxy, and its owner is the receiver’s own class. When the same question was asked of an ordinary Object.new, the author in Ruby 4.0.7 was ancestor, BasicObject, and the last line said that this definition is private. So every object already has a method_missing method, and its job is to raise NoMethodError; Proxy shadowed this method and put its own version in. The author nobody is therefore not a void but a handover: a name without a definition falls to a catcher that has one, and the catcher’s author is known. MP25. The measurement was made only for Proxy and Object; which one super reaches when there are several method_missing methods in the chain was not tested in this lesson.

Private Methods Are Not Forwarded

Because the proxy forwards the message with public_send and asks first with respond_to?, the private methods of the wrapped object cannot be called through the proxy. This is a consequence of the design and can be measured.

# private.rb — calling a private method through the proxy
require_relative "authors"

class Box
  def shown = "shown"

  private

  def secret = "secret"
end

px = Proxy.new(Box.new)
p px.shown
begin
  px.secret
rescue NoMethodError => e
  puts e.class
end
p px.respond_to?(:secret)
p px.respond_to?(:secret, true)
"shown"
NoMethodError
false
true

The public shown method passed, and the private secret raised NoMethodError. The last two lines part ways. respond_to?(:secret) gave false, because respond_to_missing? passes the second argument on to the target, and by default only the public methods of the target count. respond_to?(:secret, true) gave true: when private methods are asked about too, the Box object recognized secret. Still, px.secret did not work. The call through reflection parts ways here: reflection may say “this name can be answered”, but the public_send that carries the call refuses a private method all the same. The proxy carries the public face of the object it wraps, not all of it; this is sometimes the wanted result, because wrapping does not leak private details to the outside. MP26.

Silent Swallowing

If the super call is forgotten while writing method_missing, every message is caught and does not turn into an error. The class below does not call super and returns nil for every message.

# silent.rb — a method_missing that does not call super
class Silent
  def method_missing(*) = nil
end

s = Silent.new
p s.typo_name
p s.respond_to?(:typo_name)
nil
false

There is no method named typo_name anywhere, but the call returned nil and raised no error. The error report was our last assurance that we had spelled the name right, and here it was swallowed. For this reason, leaving a message the method does not recognize to super is a rule when writing method_missing, not a preference. Also, respond_to? gave false: this class answers the message, but does not tell reflection so. MP27. Since this class swallows every message, no name raises an error; in a real class the catching is tied to a condition such as a prefix or a wrapped object.

Telling Reflection

When the catching is conditional, reflection must be informed too. The class below catches the names that begin with the find_ prefix; it is measured first without respond_to_missing?, then with it.

# finder.rb — without and with respond_to_missing?
class Finder
  def method_missing(name, *)
    return super unless name.start_with?("find_")

    name.to_s.delete_prefix("find_")
  end
end

f = Finder.new
p f.find_amy
p f.respond_to?(:find_amy)
begin
  f.method(:find_amy)
rescue NameError => e
  puts e.class
end

class Finder
  def respond_to_missing?(name, include_private = false) = name.start_with?("find_") || super
end

p f.respond_to?(:find_amy)
p f.method(:find_amy).call
"amy"
false
NameError
true
"amy"

The find_amy message gave "amy", but respond_to? came out false and method(:find_amy) raised NameError. So an object that answers the message did not, according to reflection, recognize the method. When the class was reopened with respond_to_missing?, both questions were fixed: respond_to? gave true, and method(:find_amy).call gave "amy". The rule is this: whoever writes method_missing must write respond_to_missing? too. Otherwise the object hides from reflection. MP28. The prefix condition was measured only with find_; the catching condition and respond_to_missing? were written in the same form, and if they are written separately, an inconsistency can arise.

The Bounding Measurement

Is the author nobody permanent? The class below turns every name it catches into a real method with define_method on the first call. Thus the catching runs only once.

# limit.rb — a self-defining proxy
require_relative "authors"

class Squarer
  attr_reader :misses

  def initialize
    @misses = 0
  end

  def method_missing(name, *)
    return super unless name.start_with?("square_")

    @misses += 1
    n = name.to_s.delete_prefix("square_").to_i
    self.class.define_method(name) { n * n }
    n * n
  end

  def respond_to_missing?(name, include_private = false) = name.start_with?("square_") || super
end

sq = Squarer.new
p [sq.square_4, sq.square_4, sq.square_5, sq.misses]
p author(sq, :square_4)
p author(sq, :square_9)
[16, 16, 25, 2]
["code", "Squarer, block body"]
["nobody", "no definition"]

square_4 was called twice, but method_missing ran only once: the method was defined on the first call, and the second call reached the definition directly. misses became two, because square_4 and square_5 were each caught once. Then author classified square_4 as code; the method is now a real definition produced from a block. square_9, which was never called, is still nobody. So the author nobody is not permanent: the same name can turn into code after one call. The category is a property of the definition table at that moment, not of the name. MP29. A single class was measured; calling define_method through self.class is a design choice, and defining a method for a single instance only is possible too.

Summary

  • A message with no definition in the chain is handed to the object’s method_missing method; Proxy forwards the message that reaches the array it wraps in this way.
  • The author of caught names is nobody: the author helper finds no name in the definition table, but the message is still answered; the catching method_missing is the programmer and shadows the private default in BasicObject.
  • respond_to_missing? tells reflection about the catch; Proxy.method_defined? still stays false.
  • A method_missing without a super call swallows every message and hides typos.
  • The proxy does not take the place of the type it wraps: is_a?(Array), == and to_s are definitions in the chain and are not caught; private methods are not forwarded with public_send either.
  • The author nobody is not permanent: in a proxy that defines itself with define_method on the first call, the same name turns into code.

Next Step

The last observation of this lesson showed that a name can be nobody first and code afterward. The closing lesson of the course builds this transition on purpose: a single declaration line such as field :price, 0 gives birth to methods behind it. Domain-Specific Languages asks which author this declaration hides.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close