MetaRuby - Calling and Receiving Methods


Last time we monkeyed around with Method objects, now we’ll look at some patterns for calling and receiving methods using send and method_missing. We’ll wrap up by implementing a CollectionProxy which lets us easily operate on collections of objects.

Calling Methods

Ruby lets you call methods on an object dynamically by using send. Take a look at the small_talk method below:

class Person
  def initialize(name)
    @name = name
  end
  
  def small_talk(other)
    greet(other)
  
    topics = methods.grep /^chat_about_/
    topics.shuffle!
    topics.each{|topic| self.send topic }
  end
  
  def greet(other)
    puts "Hi #{other}, I'm #{@name}"
  end
  
  def chat_about_weather
    puts "Hmm... storm's a coming."
  end
  
  def chat_about_sports
    puts "Did you see the game last night?"
  end
  
  def chat_about_movies
    puts "Seen any good movies lately?"
  end
  
end

wallace = Person.new("Wallace")
wallace.small_talk("Fenwick")
# Hi Fenwick, I'm Wallace
# Seen any good movies lately?
# Hmm... storm's a coming.
# Did you see the game last night?

Let’s take small_talk apart and see what’s going on in there. First there’s just a normal method call to greet(other).

Next, we use methods on our Person instance to return all the method names as symbols. If you’re ever poking around in irb, this can be very handy. We take those method names and filter them down with grep to just match the ones starting with “chat_about”. Those are shuffled in place with shuffle!

Each of the methods in topics is then called using send. These methods didn’t take any arguments, but if they had we could have passed them in. For instance we could have called greet this way:

wallace.send :greet, "Fenwick"
# Hi Fenwick, I'm Wallace

By using send, we were able to call methods on this object just by knowing their names.

Receiving Methods

Ruby also provides method_missing, which allows you to control what happens when a method is not defined. Let’s extend our Person from above so that if you call a chat_about method that isn’t defined, he’ll say something instead of crash:

class Person
  #...
  
  CHAT_METHOD_REGEXP = /^chat_about_(.+)/
  
  def method_missing(name, *arguments)
    if name =~ CHAT_METHOD_REGEXP
      puts "I don't really know much about #{$1}."
    else
      super
    end
  end
end

wallace = Person.new("Wallace")
wallace.chat_about_pastries
# "I don't really know much about pastries."
wallace.sing_a_song
# NoMethodError: undefined method `sing_a_song'

Person’s method_missing now handles any call that starts with chat_about. Other method calls are passed on to the super class where it may define its own method_missing. This gives you a lot of flexibility. Just like send, the remaining arguments are passed in as well.

If you define method_missing, you should override respond_to?. Calling wallace.respond_to? :chat_about_pastries returns false, ideally it should return true. Let’s fix that:

class Person
  #...
  
  def respond_to?(name)
    if name =~ CHAT_METHOD_REGEXP
      true
    else
      super
    end
  end
end

wallace = Person.new("Wallace")
wallace.respond_to? :chat_about_pastries  #=> true
wallace.respond_to? :chat_about_weather   #=> true
wallace.respond_to? :sing_a_song          #=> false

Now our class will play better with other objects.

CollectionProxy

Now that we’ve seen how to use send and method_missing, let’s put this to good use. We’ll build a CollectionProxy which helps us work with collections of Ruby objects.

Any methods it doesn’t support should be passed on to the items in the collection. For instance, if I have a bunch of strings, and I call upcase on the CollectionProxy, we want to get all the uppercase strings. If you’re familiar with jQuery and its chaining behavior, our CollectionProxy will behave similarly.

class CollectionProxy < Object
  include ::Enumerable
  
  def initialize(collection)
    @collection = collection
  end
  
  def each(&block)
    @collection.each(&block)
  end
  
  def to_a
    @collection
  end
  
  def inspect
    details = @collection.inspect
    "#<CollectionProxy: #{details} >"
  end
  
  def method_missing(name, *arguments)
    mapped_items = @collection.map{|item| item.send name, *arguments }
    ::CollectionProxy.new(mapped_items)
  end
  
  def respond_to?(name)
    @collection.all?{|item| item.respond_to?(name) }
  end
end

colors = CollectionProxy.new(["red", "green", "blue"])
colors.upcase.reverse
#=> #<CollectionProxy: ["DER", "NEERG", "EULB"] > 

numbers = CollectionProxy.new([3,4,5])
(numbers + 2).reduce{|sum, item| sum + item }
#=> 18

A few paragraphs back, you might have called this magic, but now we have all the tools to understand what’s going on.

Let’s review the examples. In the first example, we created a CollectionProxy with three strings. Next we called upcase on it. CollectionProxy doesn’t define upcase, so Ruby calls method_missing. In this case, name is :upcase and *arguments is an empty array. We use send to forward this method call on to each of items in the collection. The result is wrapped in a new CollectionProxy.

The same thing happens when we call reverse on that result. In the end, we have a CollectionProxy where each string is uppercase and reversed.

Take a look at the second example. We’re operating on a CollectionProxy of numbers. The first call is +. This is a normal Ruby method, and has the signature def + (numeric). Since CollectionProxy doesn’t define this method, it will call send(:+, 2) on each of the items in the collection.

Next we call reduce on the CollectionProxy. By including Enumerable, we included reduce, so this will actually use our each method, and sum up all the numbers.

Copy and paste this class into irb and play with it a bit. How would you extend this to optionally get back a CollectionProxy of strings when you call to_s?

Recap

We’ve seen the great flexibility that Ruby provides us with send and method_missing.

  • Use send to call methods by name.
  • Use method_missing to intercept undefined method calls.
  • Implement respond_to? if you override method_missing.
  • Use method_missing and send to foward method calls.

More articles in this series