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 overridemethod_missing
. - Use
method_missing
andsend
to foward method calls.