Ruby Procs: Part I
August 30th, 2007 Posted in Object-Oriented Programming, RubyIn high school, I had a computer programming teacher who described variables as ‘mailboxes’, with values being ‘postcards’. In my four years of college, I don’t think I’ve ever heard that term or comparison again - though, in retrospect, I think the ‘mailbox’ analogy helped my understanding of more advanced variable topics, such as pointers (a postcard with another mailbox’s address) or pass-by-reference (a postcard with instructions to read another postcard at another mailbox). Interestingly enough, as I became a more advanced developer, I forgot about the mailbox analogy, because I didn’t need it anymore to understand what I was doing.
Recently, I started working with Ruby Procs. Ruby Procs aren’t explained very well, at least not in any book or website I’ve read, so I’ll do my best to explain what a Proc is. Incidentally, I find that the ‘mailbox’ analogy works pretty well again: a ‘Proc’ is a postcard with a set of instructions that the Postman must obey (or, I’d imagine, suffer some horrible fate). Come again? Maybe I need to rewind a bit and talk in code …
Anyone that has been working with Ruby for a bit is familiar with the concept of ‘yield’ and ‘blocks’. First, the block:
array = [1,2,3]
array.each do { |value| puts 'Doing stuff.' }
The code between the brackets {} is a block. This same code can be written as:
array.each do |value| puts 'Doing stuff.' end
A block is a block of code, is a block of code, is a block of code. Nothing too special see here.
But let’s combine the block of code with the yield statement:
def is_polygamous? if yield >= 1 true else false end my_wives = 5
The block of code:
is_polygamous? { my_wives }
Would thus produce true. Yield effectively takes the block of code after the method call, evaluates the code inside in the context of the calling scope, and executes it. This seems kind of pointless - why would anyone write a method this way, when we could simply have passed the value as a parameter? For starters, the code inside the block is not called until the function reaches the yield statement. Let’s look at another example:
def power_rangers_right
puts 'Go, go'
yield
end
def power_rangers_wrong(output_method)
puts 'Go, go'
output_method
end
Now let’s call each of the methods, passing puts(’Power Rangers!’) as a parameter:
power_rangers_right { puts('Power Rangers!')}
output: Go, go
Power Rangers!
power_rangers_wrong(puts(’Power Rangers!’))
output: Power Rangers! Go, go
As you can see, when you pass a parameter into a method, the code is executed before the method is actually called. When we use yield and code blocks, the execution is deferred until the method reaches the yield statement.
The reason I went into this long winded discussion about blocks and yields is because a function block is a Proc object. The function definition, power_rangers_right, implicitly carries a parameter in the form of power_rangers_right(&proc_object).
The real power behind the Proc, however, is not just that it is a block of code whose execution is deferred. The real power behind Proc is that Proc is a standard Ruby Object. Yes, that’s right, a Proc is a standard Ruby Object and can be passed in a method parameter, people - thus, the mailbox analogy applies. Let’s look at an actual non-trivial example of this in action:
def Lion
end
def TinMan
end
def Scarecrow
end
def JustinTimberlake
end
def wants?(person_type)
types_i_know = { :Lion => 'Courage', :TinMan => 'A Heart', :Scarecrow => 'Brain'}
complaint = Proc.new { raise ArgumentError("I don't know what #{person_type} wants.")}
types_i_know.find(complaint) { |person, value| person.is_a?(Object.const_for(person))}.last
end
There’s a lot of code going on here, so I’ll break down what we are trying to accomplish. We want to pass a class of person to a method wants?, which returns what that Person truly desires. To do this, we look up values against a Hash. If we do not know what the person wants, we want to raise an Exception. Line by line, this is what we are doing:
complaint = Proc.new { raise ArgumentError("I don't know what #{person_type} wants.")}
Remember what I said about Procs just being standard Ruby Objects? Here we are setting the Proc to a variable complaint. We will not raise the Exception unless someone calls this Proc.
types_i_know.find(complaint) { |person, value| person.is_a?(Object.const_for(person))}.last
Lots of Ruby shorthand going on here. We want to look for the String representing what a person wants, so we want to go through the Hash values and return the first value for which the conditional in the block returns true. To do this, we use the following:
find(value) { |i| condition_with_i }
Find iterates over the contents of the Enumerable, returning the first Object which fulfils the conditions set forth. If no conditions match, ‘value’ is returned. Thus, the code:
array = [1, 2, 3]
array.find(100){ |i| i % 2 =0}
returns 2, and the code:
array = [1, 2, 3]
array.find(100){ |i| i % 4 =0}
returns 100.
Object.const_for(:symbol) takes a symbol and returns a class. The code:
User.find(:all)
could be written as:
type = ‘User’
Object.const_for(type.to_sym).find(:all)
Why would you want to do this? I use this almost all the time in my Rake tasks, especially if I don’t know what types of classes I will be dealing with, or if I need to read a Class name from the command line.
person.is_a?(Class)
This is a very handy method. This will return true if person is an instance of the Class or one of a subclass of Class.
Enough beating around the bush. Let’s call some code!
lion = Lion.new jt = JustinTimberlake.new wants?(lion)
=> 'Courage'
wants?(jt)
=> ArgumentError: I don't know what JustinTimberlake wants.
Wow! What did we just do here? We passed an instance of an Object to a wants? method, which in turn delegated the logic to Enumerable’s find method, returning a Proc Object throwing an Exception if we couldn’t find what the Person wanted - deferred unless no Object was actually returned by find. How insane is that?
Now go out there and write some Ruby.