Noel Rappin Writes Here

Better Know A Ruby Thing #1: method_missing

Posted on October 16, 2023


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.email/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 be roman.translate("xii"), the StringInquirer could be done with just an equality test. Your taste is going to differ here, but I think that roman.xii is kind of better than roman.translate("xii"), but roman.xii doesn’t allow for a variable as the thing to be translated, whereas you can do roman.translate(variable). I think my point is you probably have to have a translate method of some kind, and method_missing is syntactic sugar. Relatedly, Rails used to have find_by_email_and_name, which was better than the alternative until Ruby got real keyword arguments, then find_by(**kwargs) was pretty obviously better. We’ll talk about StringInquirer 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 called respond_to_missing? and have it call that logic. respond_to_missing? is also a hook method, Ruby will use it as part of respond_to?. So adding the logic there will keep respond_to? working as expected for objects that implement method_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.



Comments

comments powered by Disqus



Copyright 2024 Noel Rappin

All opinions and thoughts expressed or shared in this article or post are my own and are independent of and should not be attributed to my current employer, Chime Financial, Inc., or its subsidiaries.