Understanding method missing
In this article we are going to look at method_missing
and 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"
.
NoMethodError
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 < Stringendmood = 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 < Stringprivatedef method_missing(method_name, *arguments)puts method_name.inspectputs arguments.inspectendend
Which will print out the following:
:happy?[]
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 < Stringprivatedef method_missing(method_name, *arguments)if method_name[-1] == '?'self == method_name[0..-2]elsesuperendendend
This updated code now correctly returns true
when asking happy?
and will return false if we asked it something like sad?
.
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 happy?
or 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?
.
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 method_missing
and respond_to_missing?
.
class StringInquirer < Stringprivatedef respond_to_missing?(method_name, include_private = false)method_name[-1] == '?'enddef method_missing(method_name, *arguments)if method_name[-1] == '?'self == method_name[0..-2]elsesuperendendend
We can use the class like so:
mood = StringInquirer.new("happy")p mood.happy? # => truep mood.sad? # => falsep mood.respond_to?(:happy?) # => true
Concluding thoughts
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. I recommend Metaprogramming Ruby 2 to go deeper on this subject.