Understanding method missing
In this article we are going to look at
respond_to_missing? in order to see what they do and how they can be used. We're going to re-create the StringInquirer class in Ruby on Rails as a way to demonstrate what is happening and how it can be used in real applications.
What is StringInquirer
If you've used Rails you've probably had to check at one point or another which environment your app is running in. To do this you probably called
Rails.env.staging?. What I didn't know until recently was that
env was an instance of the StringInquirer class.
StringInquirer allows you to check if a String is equal to something by asking it
staging? rather than
env == "staging".
You've probably seen the NoMethodError exception many times while programming Ruby. The code below will produce the NoMethodError exception because the method
happy? doesn't exist.
class StringInquirer < String end mood = StringInquirer.new("happy") mood.happy?
When you call
happy? Ruby will check to see if your object "responds to" this method... or in other words, if a method exists with that name either in the current object, or any other class up the hierarchy chain.
In this case it can't find the method
happy? so it raises the exception; but Ruby allows us to implement a method called
method_missing which is called as a last ditch attempt to find a receiver for this method call.
method_missing receives the name of the method that was being called along with the rest of the arguments that were passed to the method we wanted to call.
If we add
method_missing to our class and print out the values passed to it, we can get an idea of what we're working with.
class StringInquirer < String private def method_missing(method_name, *arguments) puts method_name.inspect puts arguments.inspect end end
Which will print out the following:
Using this knowledge, we want to check if the method_name ends with a question mark and, if so, check to see if the beginning of the method name is equal to the value of our string. If it doesn't end with a question mark, we can call
super passing off responsibility to some parent class.
class StringInquirer < String private def method_missing(method_name, *arguments) if method_name[-1] == '?' self == method_name[0..-2] else super end end end
This updated code now correctly returns
true when asking
happy? and will return false if we asked it something like
Does my object respond to?
Sometimes before you call a method you want to first ask your object if it will be able to respond to this method. To do this you can use the method
respond_to? passing the method name to the method.
"Happy".respond_to?(:length) # => true
But because our question mark methods
sad? don't actually exist, it's going to give us false no matter what.
There is one other method we can implement to help us handle the case where it can't find a concrete method to call. This is
respond_to_missing? receives the name of the method and another value called include_private which we won't worry about now. In our case, we basically want to check if the method being called ends with a question mark or not.
This leads us to our final implementation of the StringInquirer class, which has implemented both
class StringInquirer < String private def respond_to_missing?(method_name, include_private = false) method_name[-1] == '?' end def method_missing(method_name, *arguments) if method_name[-1] == '?' self == method_name[0..-2] else super end end end
We can use the class like so:
mood = StringInquirer.new("happy") p mood.happy? # => true p mood.sad? # => false p mood.respond_to?(:happy?) # => true
We've covered the use of 2 methods (method_missing and respond_to_missing?) to perform a little bit of what is called "Meta Programming". Allowing our code to ask questions about itself and create new functionality on the fly.
When you want to add a little "syntactic sugar" to your code, perhaps using these 2 methods will help you achieve that. Worst case scenario is that you've come away with knowledge of how the StringInquirer class - which is part of the ActiveSupport module of Rails - works. The next time you use
Rails.env.development? you'll know what is happening.