method_missing needs respond_to_missing?¶
Level: 301 · for a reader who has met a "ghost" method such as find_by_name in a library and wants to write one that behaves
One line: method_missing(name, *args) runs only after normal lookup has failed, and it must call super for names it does not handle; on its own it leaves respond_to? and method blind, so every ghost needs a matching respond_to_missing? — while Python's __getattr__ is likewise called only on failure, but hasattr and getattr see through it with no second hook.
When Ruby cannot find a method for a message it does not raise at once: it sends method_missing with the name and the arguments, and BasicObject's version raises the NoMethodError you know. Overriding it lets an object answer names it never defined — an ORM answers find_by_name, a proxy forwards everything — and the rule is that whatever the override does not understand goes to super, so typos still fail. That alone is half a feature. respond_to? does not ask method_missing, and neither does method, so a ghost method is invisible to every caller who checks before calling. respond_to_missing? is the second half: it is what respond_to? and method consult for unknown names, and it must say true for exactly the names method_missing accepts. The program measures a plain Db without it and a ListedDb with it, then the three classic mistakes: swallowing everything, forgetting that real methods never reach the hook, and calling the hook by hand. In prose only: every ghost call is a failed lookup followed by a Ruby-level hook, which is slower than a real method, so libraries define the method on first use, as Memo does (the cost is not machine-checked here).
Verified output of method_missing_and_respond_to_missing_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. db.find_by_name("Ada") -- a ghost method {name: "Ada", lang: "Ruby"}
2. Record.new(name: "Ada").name -- attribute ghost "Ada"
3. db.nope -- super keeps the error NoMethodError: undefined method 'nope' for an instance of Db
4. Probe.new.find_by_x("Ada") receives [Symbol, :find_by_x, ["Ada"]]
5. db.respond_to?(:find_by_name) -- no respond_to_missing? false
db.method(:find_by_name) NameError: undefined method 'find_by_name' for class 'Db'
6. listed.respond_to?(:find_by_name) true
listed.method(:find_by_lang).call("Python") {name: "Guido", lang: "Python"}
7. listed.methods.include?(:find_by_name) false
8. hook_calls: 2 ghosts (rows 1, 3), then rows, rows 2 (real methods never reach the hook)
9. Swallow.new.typo -- method_missing without super nil
Swallow.new.respond_to?(:typo) false
10. memo.say_hello twice -> value, hits "HELLO" "HELLO", hits = 1
Memo.instance_methods(false).include?(:say_hello) true
11. db.method_missing(:find_by_name, "Ada") explicitly NoMethodError: private method 'method_missing' called for an instance of Db
12. from row 3: e.name / e.receiver.equal?(db) :nope / true
Reading the output¶
Rows 1 and 2 are the two shapes of a ghost: a name parsed for a field (find_by_name → :name) and a name looked up in a Hash (record.name). Row 3 is the super rule: db.nope still raises NoMethodError with the ordinary message, and row 12 (printed last, from the exception row 3 rescued) shows it carries the Symbol in name and the object in receiver. Row 4 prints what the hook receives — the name is a Symbol, and the arguments arrive with it, because in Ruby the call and the lookup are one event. Row 5 is the problem: Db has no respond_to_missing?, so respond_to?(:find_by_name) is false and method(:find_by_name) raises NameError, even though the call in row 1 worked. Row 6 is the fix: ListedDb adds a respond_to_missing? that says true for find_by_ names, and now both respond_to? and method — a real Method object you can call — work. Row 7: methods still does not list the ghost; it lists defined methods only. Row 8: by this point db has received two ghost messages (find_by_name in row 1, nope in row 3) and, just before the row, two calls to the real rows; the hook ran exactly twice — lookup succeeds for rows, so the hook is never consulted. Row 9 is the bug to avoid: a method_missing that returns nil for everything turns every typo into nil, and respond_to? still says false, so nothing warns you. Row 10 is the idiom libraries use: the first say_hello goes through the hook, which defines a real method, so the second call is ordinary and hits stays at 1. Row 11: method_missing is private (here explicitly, in BasicObject by design), so calling it directly with a receiver is itself a NoMethodError.
Compared with Python¶
Python's __getattr__(self, name) is the same hook with one structural difference: it is an attribute hook, not a call hook. db.find_by_name is a lookup that returns something, and the parentheses call it afterwards, so __getattr__ receives a str name and no arguments (row 4) and returns a callable — a closure over the field — for the call that follows. Raising AttributeError is the super (row 3), and the exception carries name and obj just as Ruby's carries name and receiver (row 12). The largest difference is row 5: hasattr is implemented as "try getattr, catch AttributeError", so a ghost is visible to hasattr and getattr with no second hook — there is no respond_to_missing? because there is nothing to fix. What Python lacks is a listing: dir(db) does not know about ghosts, and the counterpart of respond_to_missing? for listing is __dir__ (row 7), which ListedDb overrides. Row 8 measures the two Python hooks side by side: __getattribute__ runs on every attribute access, __getattr__ only when that fails — Counted saw three accesses but one ghost. Row 9 is the same bug, worse: a __getattr__ that never raises makes hasattr answer True for every name. Row 10 memoises with setattr on the instance, so the second access is a plain __dict__ hit. Row 11: the hook is a public dunder method, so db.__getattr__("find_by_name") is an ordinary call where Ruby's db.method_missing(...) is refused.
Verified output of method_missing_and_respond_to_missing_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. db.find_by_name("Ada") -- a ghost method {'name': 'Ada', 'lang': 'Ruby'}
2. Record({"name": "Ada"}).name -- attribute ghost 'Ada'
3. db.nope -- raise AttributeError keeps it AttributeError
4. Probe().find_by_x("Ada") receives ['str', 'find_by_x', 'args arrive at the call, later']
5. hasattr(db, "find_by_name") -- no extra hook needed True
callable(getattr(db, "find_by_name")) True
6. hasattr(listed, "find_by_name") True
getattr(listed, "find_by_lang")("Python") {'name': 'Guido', 'lang': 'Python'}
7. "find_by_name" in dir(db) / in dir(listed) False / True
8. Counted: real, real, ghost -> always / only_missing 3 / 1
9. Swallow().typo -- __getattr__ that never raises None
hasattr(Swallow(), "typo") True
10. memo.say_hello() twice -> value, hits 'HELLO' 'HELLO', hits = 1
"say_hello" in vars(memo) True
11. db.__getattr__("find_by_name")("Ada") explicitly {'name': 'Ada', 'lang': 'Ruby'}
12. from row 3: e.name / e.obj is db 'nope' / True
Try it¶
- Write a
Settingsobject whosemethod_missingturnssettings.host = "x"into a Hash write andsettings.hostinto a Hash read, with arespond_to_missing?that saystruefor any writer and for every key already set; showrespond_to?(:host=),respond_to?(:port),method(:host).call, and thatsettings.nope!still raises. - Build a
Proxy < BasicObjectthat forwards every message to a wrapped object throughmethod_missingand__send__, logging each name; then makerespond_to?work on it — which class'srespond_to?do you have to reach for, sinceBasicObjectdoes not have one?
Solution to 1
method_missing_and_respond_to_missing_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# method_missing_and_respond_to_missing_kata_rb.rb — an open Settings object.
class Settings
def initialize = @data = {}
def method_missing(name, *args)
if name.end_with?("=") && args.size == 1
@data[name.to_s.chomp("=").to_sym] = args.first # settings.host = "x"
elsif @data.key?(name) && args.empty?
@data[name] # settings.host
else
super # anything else is still an error
end
end
def respond_to_missing?(name, include_private = false)
name.end_with?("=") || @data.key?(name) || super
end
end
s = Settings.new
s.host = "example.org"
puts "1. s.host after s.host = \"example.org\" -> #{s.host.inspect}"
puts "2. s.respond_to?(:host=) -> #{s.respond_to?(:host=)}"
puts "3. s.respond_to?(:host) / (:port) -> #{s.respond_to?(:host)} / #{s.respond_to?(:port)}"
puts "4. s.method(:host).call -> #{s.method(:host).call.inspect}"
begin
s.nope!
rescue NoMethodError => e
puts "5. s.nope! -> #{e.class}: #{e.message}"
end
Verified output of method_missing_and_respond_to_missing_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
sendreaches private methods;public_senddoes not — sending the name thatmethod_missingwill receive- A DSL is
instance_evalplus stored blocks —method_missingas the way a DSL accepts any key define_methodturns a block into a method — howMemoturns a ghost into a real methodBasicObjectandKernel— where the defaultmethod_missinglives, and the proxy that exercise 2 asks for- Method lookup and
super— the lookup that has to fail before the hook runs - Common error messages — the
NoMethodErrorandNameErrorwordings rows 3, 5 and 11 print - Rust: "No method named …" ↗ — the language with no hook at all: an unknown method is a compile error, never a message
- A setter returns its argument, not its body —
obj.x = 5is a call to the methodx=, but as an expression it evaluates to 5 whatever the method body… - An object can list its methods and variables —
methods,instance_methods(false),instance_variables,instance_method(:x).parameters,… - There is more than one way, and one obvious way — Ruby's idioms