method_missing done right
The pair
method_missing is called when the lookup fails. Overriding it handles a name the class does not define, and respond_to_missing? answers the matching question for respond_to?. They are a pair: one without the other produces an object that lies about what it can do.
def method_missing(name, *args)
return super unless name.to_s.end_with?("=") || args.empty?
@values ||= {}
if name.to_s.end_with?("=")
@values[name.to_s.chomp("=")] = args.first
else
@values[name.to_s]
end
end
def respond_to_missing?(name, include_private = false)
name.to_s.end_with?("=") || (@values || {}).key?(name.to_s) || super
end
super in both is the part that keeps Ruby's error messages. A method_missing that never calls super turns every typo into silence.
A settings object
A settings hash with attribute-style access is the small case that earns the pair. Reading and writing work, and an unknown call with an argument still raises:
config = Config.new
config.color = "red"
p config.color
p config.respond_to?(:color)
p config.respond_to?(:nope)
begin
config.nope(1)
rescue NoMethodError => e
puts "NoMethodError: #{e.message}"
end
ruby missing.rb
"red" true false NoMethodError: undefined method 'nope' for an instance of Config
The call with an argument goes to super because the guard rejects it, so Ruby builds the usual message. The respond_to? answers are the ones a framework or a serialiser checks before it calls.
respond_to_missing?
Ruby calls respond_to_missing? only after the normal lookup fails, and only from respond_to?. It must not raise, and it must accept the second argument:
def respond_to_missing?(name, include_private = false)
name.to_s.end_with?("=") || (@values || {}).key?(name.to_s) || super
end
The method is a promise: when it answers true, a caller may call the name and expect method_missing to handle it. A promise that is broader than the handler produces a NoMethodError from code that checked first.
When it is wrong
- A fixed set of names. If the names are known, define them with define_method (next lesson). Then methods, tab completion and the documentation all see them.
- A typo. A catch-all handler turns a misspelled method into a nil that travels three frames. The super in the pair is what prevents this; a handler without it is a bug.
- A public API. A method that exists only through method_missing is invisible to a reader and to the editor. Reserve the trick for a proxy, a configuration object or a compatibility layer with a documented list.
What to watch for
method_missing on Object
changes every object in the process. A library that does it is a global
change, and two such libraries can disagree about a name.
A handler that is slow is called for every missing name, including the
ones from inspect and
respond_to? probes. Keep it to a hash lookup and
let super do the rest.
Next: define_method and small DSLs
Article author: Arthur Isaev