Ruby is a one of the most popular languages used on the web. We’re running a Session here on Nettuts+ that will introduce you to Ruby, as well as the great frameworks and tools that go along with Ruby development. In this episode, we’re going to look at the too-cool-to-be-true way that Ruby objects deal with methods that don’t exist.
Video Tutorial?
A Problem (and a Solution)
Let’s say your working with a Ruby object. And let’s also say that you aren’t entirely familiar with this object. And let’s also say that you call a method that doesn’t exist on the object.
o = Object.new o.some_method # NoMethodError: undefined method `some_method' for #<Object:0x00000100939828>
This is less than desirable, so Ruby has an awesome way of allowing us to rescue ourselves from this. Check this out:
class OurClass def method_missing (method_name) puts "there's no method called '#{method_name}'" end end o = OurClass.new o.some_method # => there's no method called 'some_method'
We can create a method called method_missing
in our class. If the object we’re calling the method on doesn’t have the method (and doesn’t inherit the method from another class or module), Ruby will give us one more chance to do something useful: if the class has a method_missing
method, we’ll hand the information about the method cal to method_missing
and let it sort the mess out.
Well, that’s great; we’re no longer getting an error message.
A Better Use
But stop and think about this for a second. First of all: no, we’re not getting an error message any more, but we aren’t getting something useful. It’s hard to say what useful would be in this case, because out method name doesn’t suggest anything. Second of all, this is pretty powerful, because it allows you to basically pass any method to an object and get an intelligent result.
Let’s do something that makes more sense; start with this:
class TutsSite attr_accessor :name, :tutorials def initialize name = "", tuts = [] @name = name @tutorials = tuts end def get_tuts_about_javascript @tutorials.select do |tut| tut[:tags].include? "javascript" end end def get_tuts_by_jeffrey_way @tutorials.select do |tut| tut[:author] == "Jeffrey Way" end end end
Here you see a little class for a tutorial website. When creating a new website object, we pass it a name and an array of tutorials. We expect tutorials to be hashes in the following form:
{ title: "Some title", author: "the author", tags: ["array", "of", "tags"] # Ruby 1.9 # OR { :title => "Some title", :author => "the author", :tags => ["array", "of", "tags"] # Ruby 1.8
We expect symbols as the keys; notice that if you’re not using Ruby 1.9, you’ll have to use the bottom format for your hashes (both work in 1.9)
Then, we’ve got two helper functions that allow us to get only the tutorial that have a JavaScript tag, or only the tutorials by Jeffrey Way. These are useful for filtering the tutorials … but they don’t give us too many options. Of course, we could make methods named get_tuts_with_tag
and get_tuts_by_author
that take parameters with the tag or author name. However, we’re going to go a different route: method_missing
.
As we saw, method_missing
gets the attempted method name as a parameter. What I didn’t mention is that it’s a symbol. Also, the parameters that get passed to the method and the block (if one was given) are available as well. Note that the parameters are passed as individual parameters to method_missing
, so the usual convention is use the splat operator to collect them all into an array:
def method_missing name, *args, &block end
So, since we can get the name of the method that was attempted, we can parse that name and do something intelligent with it. For example, if the user calls something like this:
nettuts.get_tuts_by_jeffrey_way nettuts.get_tuts_about_html nettuts.get_tuts_about_canvas_by_rob_hawkes nettuts.get_tuts_by_jeremy_mcpeak_about_asp_net
So, let’s get to it; scrap those earlier methods and replace it this this:
def method_missing name, *args, &block tuts = @tutorials.dup name = name.to_s.downcase if (md = /^get_tuts_(by_|about_)(\w*?)((_by_|_about_)(\w*))?$/.match name) if md[1] == 'by_' tuts.select! { |tut| tut[:author].downcase == md[2].gsub("_", " ") } tuts.select! { |tut| tut[:tags].include? md[5].gsub("_", " ") } if md[4] == '_about_' elsif md[1] == 'about_' tuts.select! { |tut| tut[:tags].include? md[2].gsub("_", " ") } tuts.select! { |tut| tut[:author].downcase == md[5].gsub("_", " ") } if md[4] == '_by_' end else tuts = "This object doesn't support the object '#{name}'" end tuts end
Don’t get worried, we’ll walk through all this now. We start by duplicating the @tutorials
array; every Ruby object has a dup
method that copies it; if we didn’t do this—and just said tuts = @tutorial
—we would be working with the original array, which we don’t want to do; we want to preserve that array as it is. Then, we’ll filter out the tutorial hashes we don’t want.
We also have to get the name of the method; since it’s passed to method_missing
as a symbol, we convert it to a string with to_s
and then make sure it’s in lowercase with downcase
.
Now, we have to check to see that the method matches the format we want; after all, it’s possible that someone could pass something else to the method. So, let’s parse that method name. If it matches, we’ll work out magic; otherwise, we’re return a default error message:
if (md = /^get_tuts_(by_|about_)(\w*?)((_by_|_about_)(\w*))?$/.match name) #coming else tuts = "This object doesn't support the method '#{name}'" end
That looks like a rather daunting, but you should understand it: basically, we’re looking for “get_tuts_” followed by “by_” or “about_”; then, we have an author’s name or a tag, followed by “_by_” or “_about_” and an author or tag. If that matches, we store the MatchData
object in md
; otherwise, we’ll get nil
back; in that case, we’ll set tuts
to the error message. We do this so that either way, we can return tuts
.
So the regular expression matches, we’ll get a MatchData
object. If the method name used was get_tuts_by_andrew_burgess_about_html
, these are the indices that you have:
0. get_tuts_by_andrew_burgess_about_html 1. by_ 2. andrew_burgess 3. _about_html 4. _about_ 5. html
I’ll note that if one of the optional groups isn’t filled, its index has a value of nil
.
So, the data we want is at indices 2 and 5; remember that we could get only a tag, only an author, or both (in either order). So, next we have to filter out the tuts that don’t match our criteria. We can do this with the array select
method. It passes each item to a block, one by one. If the block returns true
, the item is kept; if it returns false
, the item is thrown out of the array. Let’s start with this:
if md[1] == 'by_' tuts.select! { |tut| tut[:author].downcase == md[2].gsub("_", " ") } tuts.select! { |tut| tut[:tags].include? md[5].gsub("_", " ") } if md[4] == '_about_'
If md[1]
is “by_”, we know the author came first. Therefore, inside the block of the first select
call, we get the tut
hash’s author name (downcase
it) and compare it to md[2]
. I’m using the global substitution method—gsub
—to replace all the underscores with a single space. If the strings compare true, the item is kept; otherwise it’s not. In the second select
call, we check for the tag (stored in md[5]
) in the tut[:tags]
array. The array include?
method will return true
if the item is in the array. Notice the modifier on the end of that line: we only do this if the fourth index is the string “_about_”.
Notice that we’re actually using the array select
method: we’re using select!
(with a bang / exclamation mark). This doesn’t return a new array with only the selected items; it works with the actual tuts
array in memory.
Now that you understand that, you shouldn’t have a problem with the next lines:
elsif md[1] == 'about_' tuts.select! { |tut| tut[:tags].include? md[2].gsub("_", " ") } tuts.select! { |tut| tut[:author].downcase == md[5].gsub("_", " ") } if md[4] == '_by_' end
These lines do the same as above, but they’re for method names in the reverse situation: tag first, optional author second.
At the end of the method, we return tuts
; this is either the filtered array, or the error message.
Now, let’s test this:
tuts = [ { title: "How to transition an Image from B&W to Color with Canvas", author: "Jeffrey Way", tags: ["javascript", "canvas"] }, { title: "Node.js Step by Step: Blogging Application", author: "Christopher Roach", tags: ["javascript", "node"] }, { title: "The 30 CSS Selectors you Must Memorize", author: "Jeffrey Way", tags: ["css", "selectors"] }, { title: "Responsive Web Design: A Visual Guide", author: "Andrew Gormley", tags: ["html", "responsive design"] }, { title: "Web Development from Scratch: Basic Layout", author: "Jeffrey Way", tags: ["html"] }, { title: "Protect a CodeIgniter Application Against CSRF", author: "Ian Murray", tags: ["php", "codeigniter"] }, { title: "Manage Cron Jobs with PHP", author: "Nikola Malich", tags: ["php", "cron jobs"] } ] nettuts = TutsSite.new "Nettuts+", tuts p nettuts.get_tuts_by_ian_murray # [{:title=>"Protect a CodeIgniter Application Against CSRF", :author=>"Ian Murray", :tags=>["php", "codeigniter"]}] p nettuts.get_tuts_about_html # [{:title=>"Responsive Web Design: A Visual Guide", :author=>"Andrew Gormley", :tags=>["html", "responsive design"]}, {:title=>"Web Development from Scratch: Basic Layout", :author=>"Jeffrey Way", :tags=>["html"]}] p nettuts.get_tuts_by_jeffrey_way_about_canvas # [{:title=>"How to transition an Image from B&W to Color with Canvas", :author=>"Jeffrey Way", :tags=>["javascript", "canvas"]}] p nettuts.get_tuts_about_php_by_nikola_malich # [{:title=>"Manage Cron Jobs with PHP", :author=>"Nikola Malich", :tags=>["php", "cron jobs"]}] p nettuts.submit_an_article # This object doesn't support the method 'submit_an_article'"
I’m p
-rinting out the results from these methods, so you can run this in a ruby file on the command line.
A Warning
I should mention that, while this is pretty cool, this isn’t necessarily the right use of method_missing
. It’s there primarily as a safety to rescue you from errors. However, the convention isn’t bad: it’s widely used in the ActiveRecord
classes that are a big part of Ruby on Rails.
A Bonus
You probably didn't know that there was a similar feature in JavaScript: it's the __noSuchMethod__
method on objects. As far as I know, it's only supported in FireFox, but it's an interesting idea. I've re-written the example above in JavaScript, and you can check it out at this JSBin.
Conclusion
That’s a wrap for today! I’ve got some interesting Ruby stuff up my sleeve, coming for you soon. Keep your eye on Nettuts+, and if you want something specific, let me know in the comments!
Comments