Saturday, January 27, 2007

Ruby: Invoking a method with a ampersand-parameter

On a few ocassions recently I've been asked what the & does when it is used in the context of a method invocation parameter. For example, consider the following code, specifically line 6 where collect is called.
1. # traditional
2. [1,2,3].collect { |number| number % 2 } #=> [1,0,1]
3.
4. # collect with a previously created block
5. block = lambda { |number| number % 2 }
6. [1,2,3].collect &block #=> [1,0,1]
It's fairly easy to see what is going on from the above example; however, more often I run into code that looks like the following code.
def clone_collect(&block)
clones = collect { |item| item.dup }
clones.collect &block
end
The previous example shows a method that clones each item and then passes on the block to the collect method of the clones array. The above code seems to be a tripping point, I assume it's because the block is defined outside the method definition.

Using a ¶meter with a method invocation is explained very well within PickAxe:
Invoking a Method
[ receiver. ] name [ parameters ] [ block ] 
parameters: ( [ param, ... ] [ , hashlist ] [ *array ] [ &a_proc ] )
block: { blockbody }
do blockbody end
...

A block may be associated with a method call using either a literal block (which must start on the same source line as the last line of the method call) or a parameter containing a reference to a Proc or Method object prefixed with an ampersand character.
Another good explanation can also be found in PickAxe:
If the last argument to a method is preceded by an ampersand, Ruby assumes that it is a Proc object. It removes it from the parameter list, converts the Proc object into a block, and associates it with the method.
An understanding of the above is crucial if you want to comprehend how the following code works within a Rails codebase.
area_codes = phone_numbers.collect &:area_code
Reducing the following statement you can see that area_codes is being set equal to the result of collect being called on phone_numbers. The phone_numbers variable is an array of PhoneNumber instances. The PhoneNumber class has an area_code attribute.

So, the only mystery is what does &:area_code do? As previously stated, if the last parameter is prefixed with an &, ruby removes it from the parameter list, and converts the Proc object into a block. The conversion from a parameter into a block is done through the to_proc method of the parameter. Therefore, by defining the to_proc method it is possible to alter the behavior of any parameter passed to a method that expects a block.
class Greeter
def to_proc
lambda { "hello world" }
end
end

def greet
yield
end

greet &Greeter.new #=> "hello world"
Applying this idea to the String class you could define to_proc as below and create blocks using strings and eval.
class String
def to_proc
text = self
lambda { eval text }
end
end

instance_eval &"2 + 2" #=> 4
Along the same lines, Rails defines the Symbol.to_proc method to create a new proc which takes args. The first element of args is expected to be the item from the collection. The item from the collection is sent the symbol (self from the code below) which is the symbol that specifies which attribute to return. The full code can be seen below.
class Symbol
def to_proc
Proc.new { |*args| args.shift.__send__(self, *args) }
end
end
The result is a proc that allows you to iterate the collection and return an array of the attribute that is specified by the symbol (:area_code in our example).

The following code should be fully executable and show the concept in entirety.
class Symbol
def to_proc
Proc.new { |*args| args.shift.__send__(self, *args) }
end
end

PhoneNumber = Struct.new :area_code
*phone_numbers = PhoneNumber.new(904), PhoneNumber.new(646), PhoneNumber.new(616)
area_codes = phone_numbers.collect &:area_code #=> [904, 646, 616]
Post a Comment