Better Know A Ruby Thing #1: method_missing
Welcome to "Better Know A Ruby Thing". In each one of these, we're going to look at some feature of Ruby language, library, ecosystem, or culture and explain what it does, how it works, why it's there, and any thing else that comes to mind.
First up, method_missing
. If I may be poetic for a second, method_missing
represents both infinite potential, and the possibility of a second chance when you can't figure out what to do the first time around.
Okay -- that's maybe a lot to hang on a hook method, but I do think the way that Ruby uses method_missing
to make infinite API's not just possible but easy to write is very basic to what I think of as the Ruby aesthetic. It's also not something that everybody loves, and it's something that you can get into trouble with.
Hey, if you like this and want to see more of it in your mailbox, you can sign up at http://buttondown.com/noelrap. If you really like this and would like to support it financially, thanks, and you can sign up for a Monthly Subscription for $3/month or an Annual Subscription for $30/year.
Also, you can buy Programming Ruby 3.2 in ebook from Pragmatic or as a pre-order from Amazon, coming sometime in November.
Thanks!
What is method_missing?
method_missing
is, itself, a normal Ruby method. You can call it directly -- well, kind of. It's a private method, but nothing in Ruby is really private:
irb(main):037> x = "test"
=> "test"
irb(main):038> x.send(:method_missing, :banana)
(irb):38:in `<main>': private method `banana' called for "test":String (NoMethodError)
Note that the error message says the name of the method it can't find is banana
, and not method_missing
, that's because we've actually called method_missing
directly, with banana
as an argument, and Ruby treats that as though we intended to call a method named banana
. method_missing
is one of about a dozen methods in Ruby that are instance methods of BasicObject
, meaning that it's available anywhere in Ruby.
More to the point, method_missing
is a "hook" method, meaning that it is automatically called by Ruby itself under certain circumstances. Specifically, method_missing
is called by Ruby when method lookup fails. Rather than just throw an error if Ruby can't find a method, Ruby does an entire other method lookup for method_missing
. If no other class or module in the method lookup path defines method_missing
, then Ruby eventually will get to the BasicObject#method_missing
implementation, which always exists, and is where the actual NoMethodError
is raised.
If you do implement method_missing
in the load path, then it's called by Ruby with the original method name (as a symbol) as the first argument and any other arguments following exactly as the original method was called, positional arguments, keyword arguments, block argument.
irb(main):039* class NothingGoesMissing
irb(main):040* def method_missing(name, *args, **kwargs, &block)
irb(main):041* p "method #{name} is not missing"
irb(main):042* end
irb(main):043> end
=> :method_missing
irb(main):044> ngm = NothingGoesMissing.new
=> #<NothingGoesMissing:0x00000001045b8df8>
irb(main):045> ngm.banana
"method banana is not missing"
=> "method banana is not missing"
How is method_missing normally demonstrated?
A common way to demonstrate method_missing
is with a Roman numeral class, which has the advantage of being a) something many readers will already have some knowledge of and b) a case where a nigh-infinite API might make some sense. It's harder to come up with these examples than you might think...
The one in the official Ruby docs looks more or less like this -- it's implementing a Roman to decimal translator, so we're creating basically empty instances that just do translation. It's a weird API, but it's simple and focuses on the method_missing
:
class Roman
def to_int(str)
# hand wave this, but assume it raises an error
# if the string isn't translatable
end
def method_missing(numeral, ...)
to_int(numeral.to_str)
rescue
super
end
end
roman = Roman.new
roman.iv #=> 4
There are a couple of things about this demo code that I don't love. The basic idea, though, is fine. Any method name that doesn't already exist gets shuttled through method_missing
, where the class attempts to convert it to an integer. If it can, the method returns that value. If not, the to_s
method throws an error, method_missing
catches the error and calls super
, allowing for normal processing, in this case meaning we bounce back to BasicObject#method_missing
and throw an error (super
without arguments passes the original arguments).
This is a reasonable use of method_missing
, though in practice I'd probably make it a class method so you could do RomanNumeral.xvi
or whatever. You can do that by defining self.method_missing
inside a class definition. (There's a related const_missing
, we'll probably discuss that in a future Better Know about constants...)
Do you have a favorite use of method_missing?
Glad you asked.
I have a deep and abiding love for Rails ActiveSupport's StringInquirer
. It's not much of an exaggeration to say this snippet represents what I enjoy about the Ruby aesthetic.
class StringInquirer < String
private
def respond_to_missing?(method_name, include_private = false)
method_name.end_with?("?") || super
end
def method_missing(method_name, *arguments)
if method_name.end_with?("?")
self == method_name[0..-2]
else
super
end
end
end
enironment = StringInquirer.new("production")
environment.production? #=> true
environment.dev? #=> false
I have taught at least one Ruby/Rails workshop where people in the class burst into laughter on seeing this code. Whether it was for "that's clever" reasons or "cute little language you've got here" reasons, I leave to your imagination.
If you have a StringInquirer
instance, it behaves like a regular string except that if you pass it an unknown method, it uses the method_missing
check. In that check, if the method name does not end in ?
, we just call super
and likely get an exception raised. If the method name does end in ?
we compare the value of the string (self
) with the method name minus the last character (method_name[0..-2]
), returning true if they are the same.
I unequivocally love this, because of, and not in spite of, the way it invokes the deepest kinds of Ruby dynamism for the dubious aesthetic advantages of being able to say Rails.env.production?
rather than Rails.env == "production"
. I use StringInquirer
(and it's cousin ArrayInquirer
) every chance I get (I do tone it down a bit when working with a team, I'm not a monster).
A sidebar is that literally as I was writing this, I learned that since Rails 6.1, Rails actually defines a class EnvironmentInquirer < StringInquirer
that effectively memoizes the three default environment methods, like test?
, but still uses method_missing
for other environments, as a performance optimization. The more you know...
Do other languages have method_missing?
More than you might think.
Most dynamic languages have it in some form, though I think Ruby promotes it the most. In Smalltalk, the method hook is called doesNotUnderstand
, and it behaves similarly. JavaScript used to sort of have a __noSuchMethod__
, looking now it seems like you can kind of do this with proxies? In Python, you can do some of this with the __getattr__
hook (which is also often demonstrated with Roman numerals...). Other late-binding languages have various and sundry ways to kind of catch method names before throwing their hands up in despair and raising an exception.
When should you use method_missing?
My decision checklist for using method_missing
goes something like this:
- Is there a pattern of related behavior such that I can derive the behavior from the name I might give to that behavior? For the Roman numeral example, this behavior is "convert to integer" and is named by the Roman numeral. For
StringInquirer
, the behavior is "equality test" and the name is "the thing I am testing against". Sometimes the behavior is "I'm delegating a method" and the name is "the method I'm delegating". - Is the number of potential patterns nigh-infinite or at least quite large? If not, if there's a tractable number of them, then you might be better off using a loop with
define_method
to define all the possible options. - Is the
method_missing
API better than the alternative API? There's almost always an alternative implementation. The Roman numeral case could beroman.translate("xii")
, theStringInquirer
could be done with just an equality test. Your taste is going to differ here, but I think thatroman.xii
is kind of better thanroman.translate("xii")
, butroman.xii
doesn't allow for a variable as the thing to be translated, whereas you can doroman.translate(variable)
. I think my point is you probably have to have atranslate
method of some kind, andmethod_missing
is syntactic sugar. Relatedly, Rails used to havefind_by_email_and_name
, which was better than the alternative until Ruby got real keyword arguments, thenfind_by(**kwargs)
was pretty obviously better. We'll talk aboutStringInquirer
in a second.
A common use case for method_missing
is delegation -- you have an object that is effectively a wrapper around another object and you want to transparently pass through a set of methods to the other object.
It's probably worth mentioning that Ruby already provides standard classes like SimpleDelegator
(which admittedly uses method_missing
) which are worth looking at for your use cases. If the set of methods is smallish, looping over the method names and creating a define_method
will also work.
I've also seen method_missing
used as an error catcher -- if you either want to produce a different error than NoMethodError
or if you want to make sure that something happens before the error is thrown.
The last time I actually used method_missing
in the wild, was kind of as a stunt, but if you read that article, you'll find I actually wound up finding the direct API kind of useful. It turned out that project.end_of_two_weeks_ago
is arguably an easier API to use than either project.at("end of two weeks ago")
or project.at(2.weeks.ago.end_of_week)
.
How can you use method_missing responsibly?
There's also a checklist of things that you should do to use method_missing
without causing trouble.
- Take the logic that determines what methods that your
method_missing
will actually respond to. Put that logic it it's own method calledrespond_to_missing?
and have it call that logic.respond_to_missing?
is also a hook method, Ruby will use it as part ofrespond_to?
. So adding the logic there will keeprespond_to?
working as expected for objects that implementmethod_missing
The skeleton here is something like:
class MissingUser
def respond_to_missing?(method_name, ...)
# return true if method_missing will use this
end
def method_missing(method_name, ...)
super unless respond_to_missing?(method_name)
# logic here
end
end
You will possibly need to name further arguments if you want to use them, it's worth mentioning that if the method call has a block argument, that block is also available to method_missing
.
Another fun thing you can do is create a real method from method_missing
-- this is valuable if you think method_missing will be called multiple times with the same method name and you want a slight speed boost from not having to do the whole lookup each time.
The trick here is that you need to make sure define_method
is called on the class, rather than the instance:
class Creator
def method_missing(method, *args, **kwargs, &block)
p "in method missing with #{method}"
self.class.define_method(method) do |*args, **kwargs, &block|
p "in the defined method for #{method}"
end
send(method, *args, **kwargs, &block)
end
end
irb(main):032> c = Creator.new
=> #<Creator:0x00000001044d0b98>
irb(main):033> c.test
"in method missing with test"
"in the defined method for test"
=> "in the defined method for test"
irb(main):034> c.test
"in the defined method for test"
=> "in the defined method for test"
The second call, which doesn't go through method_missing
is presumably slightly -- the first call is slightly slower than just doing method_missing
so you want this only if the method is going to get called a lot.
How can you use it responsibly, long-term?
One of the knocks against using method_missing
is that the resulting methods aren't searchable -- if you use StringInquirer
, you can't search your code for the definition of production?
.
I have mixed feelings about this argument. One the one hand, it's clearly a problem. There are all kinds of developer tools that are predicated on the idea that methods are actually defined some place that the tool can find them. Developers understandably get a little antsy if they put production?
in the search box and get no results.
On the other hand, one running theme of this series is going to be using dynamic tools to solve dynamic problems. If you go into a Rails console and type Rails.env.method(:test?)
, Ruby tells you that it's a method of EnvironmentInquirer
. Sadly, the Method#source_location
method isn't as helpful as you would like. But if you have the method in a stack trace in the debugger, the stack trace will take you to the lines in method_missing
that are being executed.
A problem here is that often method_missing
is used to simulate an infinite API, and you just can't document, say, all the possible Roman numerals you might use. The non-method missing api of roman.translate("xii")
does have an advantage here, in that translate
is a method that you can document. There's a related argument here about whether it's ever a good idea to have an infinite API -- I think it's fine in some circumstances, but you'll have no problem finding people willing to take the opposite side in that argument.
It's certainly possible to find the method_missing
API easier to use or more flexible, but you want to make sure that you document the pattern (having a separate respond_to_missing?
that encapsulates the logic is helpful here), and you want good test cases that show examples. If someone uses method
to find the file where the method_missing
is defined, you want to give them a fighting chance of being able to figure out what's going on.
Another problem with method_missing
is that it gets called after regular method lookup, and that can lead to some weird effects.
A quick one is
x = ActiveSupport::StringInquirer.new("empty")
x.empty? # => false
Since empty?
is already defined for strings, the StringInquirer
never gets that chance to do its comparison. If Ruby were ever to add String#test?
, for some weird reason, that would break some Rails code.
Rails 7.1 added Object#with
and I know for sure there is at least one gem in the Rails ecosystem that is using method_missing
to delegate a with
method, and that just breaks, which is not great.
Do I have a hot take?
My general hot take is that Ruby devs should be more open to using method_missing
where it saves effort. I think this is mostly in library code, where you might want the flexible, syntactic sugar API. But I also think you should always wrap third-party external objects in local wrappers, and method_missing
(or Delegator
) can be helpful there as well.
I have a specific argument that the StringInquirer
usage of method_missing
makes sense on the merits for use in querying Rails environments.
There are two primary alternate implementations -- you can create an environment object that explicitly defines a limited set of methods like test?
or you can just ask people to use equality, == "test"
.
Each of those has a potential problem. The explicit method version limits the names of the environments that it knows about. If you have environments named "staging" or "qa" or "api" or whatever, you need to somehow patch the environment method (which is, to be clear, doable).
The equality method has a subtler problem, which is type related -- if the environment is stored as a string, then == :test
will always be false. (Again, you can get around this, by having an environment object define ==
to take symbols or string arguments.)
Whatever else you can say about the method_missing
solution, it has neither of those issues. New environment names are just available, and since the test is a method name, the type check is not an issue (another example of using dynamic tooling to manage dynamic problems).
And So...
You should now Better Know method_missing
. Try it somewhere, even if it's just some side code. The best way to get a sense of your actual comfort level with Ruby's dynamic features is to see what they look like in your code and whether they can solve your problem.
If you want to comment on this email, head for its web archive location at http://www.noelrappin.com/blog/2023/10/better-know-a-ruby-thing-10-method_missing
Dynamic Ruby is brought to you by Noel Rappin.
Comments and archive at noelrappin.com, or contact me at noelrap@ruby.social on Mastodon or @noelrappin.com on Bluesky.
To support this newsletter, subscribe by following one of these two links: