Advertisement

Ruby for Newbies: Missing Methods

by
This post is part of a series called Ruby for Newbies.
Ruby for Newbies: The Tilt Gem
Ruby for Newbies: Testing with Rspec

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?

Subscribe to our YouTube and Blip.tv channels to watch more screencasts.

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!

Advertisement