Ruby Method Mising

October 17, 2016

A little innocuous method method_missing snuck into a problem in my Bloc Software Engineering homework. Oh you know, just implement method_missing for a find() method in our ActiveRecord-ish ORM experiment. I was referred to this link which is a nice, concise explanation.

Saving you the click, basically the method_missing method is designed to deal with the NoMethodError exception that is raised when code calls a method that can't be found. Our method_missing function allows for a graceful exception handling and potential recovery. In my problem set, I am supposed to implement this such that Entry.find_by(:name, name) and Entry.find_by_name(name) produce identical results.

A lovely example was presented but as is often the case, the example code is too simple for my tiny, dinosaur brain. I actually need to hop into IRB to understand it.

So let's play around in console.

2.3.0 :013 > class Person
2.3.0 :014?>   def hey
2.3.0 :015?>     puts "hey"
2.3.0 :016?>     end
2.3.0 :017?>   def bye
2.3.0 :018?>     puts "bye"
2.3.0 :019?>     end
2.3.0 :020?>   def method_missing(m, *args, &block)
2.3.0 :021?>     puts "no #{m} here"
2.3.0 :022?>     end
2.3.0 :023?>   end

2.3.0 :024 > bob = Person.new
 => #<Person:0x007fb6c9a7b8e0>

2.3.0 :027 > bob.fcks_given
no fcks_given here
 => nil

So this is the basic idea - we call a method that is not defined in the class, and we get a nice graceful message.

But I am supposed to be able to make a method Entry.find_by_name(name) fire off. Now note that only find_by exists in code. Naturally I need to do a bit more research to figure out the best approach for a solution.

Fortunately, I have bought a dozen Ruby books, and the gem of a book Eloquent Ruby has an entire chapter devoted to method_missing.

The first thing to know is that method_missing is pure Ruby magic. When a function is called and is not found in the inheritance tree, the party does not just stop. Ruby then calls method_missing secretly in the background. What we do when we implement method_missing is we are overriding the default behavior. That is important to know if we are going to be customizing the method, because you definitely some sort of default behavior for non-explicit cases.

Now I think I figured this out for the quick'n'dirty solution, being sure to define in the same class as my find_by method:

 def method_missing(method, *arguments, &block)
   if method == "find_by_name"
     find_by(:name, *arguments)
   else
     puts "no such method found"
   end
 end

So we can satisfy the problem set's solution and see how this method can serve to provide your own syntactic sugar within your code - such as providing custom method names for search, here - without repeating method definitions.

Per my amazing book on Eloquent Ruby, there is even more that you can do with method_missing besides customizing an already robust error catching method. You can also use method_missing for delegation.

Let's say we have a class Spreadsheet and we also have a class SecretSpreadsheet that needs to authenticate the user attempting to view a Spreadsheet before handing off control. If Spreadsheet has 25 methods, we would need to write 25 methods in SecretSpreadsheet that first authenticate and then lastly pass off to the same-named method in Spreadsheet which is repetitive and lame. It does not scale well as a solution at all - every new method on Spreadsheet needs an analog method in SecretSpreadsheet.

So the solution is to use method_missing as a catch-all that will authenticate and then use our new best friend method send to call the method. This is an exceptional use case for send as obviously we can't call a method in the form of a variable in the syntax @spreadsheet.variable_name - we need to use variable.send(method, *arguments) and then we don't need to write 25 methods in SecretSpreadsheet - we only write what we need for the security routines and then pass the method and arguments onto our original Spreadsheet class. The trick is send allows us to call functions dynamically and programmatically.

What happens if a super common method like to_s is called on a SecretSpreadsheet object? It will never hit method_missing because it isn't missing! The book suggests you have your SecretSpreadsheet document inherit from BasicObject which is such a foundational, atomic class that it practically has no methods, which avoids all this trouble. Now even common methods will be unknown and trip the method_missing.