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]

9 comments:

  1. At the Rails Edge conference today, Dave Thomas called the &:xx syntax the "Blockinator". Great stuff.

    His example:
    %w{ cat dog}.map{|word| word.upcase}
    => ["CAT", "DOG]

    versus

    %w { cat dog }.map(&:upcase)
    =>["CAT", "DOG"]

    ReplyDelete
  2. That's pretty nifty and impressive! But it also makes me think of an Agile practice as suggested in "Practices of an Agile Developer" by Venkat and Andy: Write code to be clear, not clever.

    If the block way works and isn't too much trouble, why go for the "blockinator" which is difficult to understand (the reason this blog entry was written) and dunno what advantages it brings?

    ReplyDelete
    Replies
    1. Well, I don't find it difficult to understand.

      One advantage (in my opinion) is that, as a programmer, I don't have to think about choosing yet another local variable.

      Also, I find

      well_named_collection.map &: well_named_operation

      very readable, certainly more readable than

      well_named_collection.map{ |e| e.well_named_operation }

      because in this case, I find the braces part to be… well… syntactical overhead.
      YMMV, of course.

      Delete
  3. Aman,
    I've seen this discussion elsewhere, but I can't remember so I'll have to quote without knowing the source. It's an opinion I generally agree with.

    Something along the lines of:
    As of right now the %w[cat dog].collect &:upcase syntax is less readable, and thus undesirable. However, if enough people use it, it becomes an idiom and then it is preferred.

    ReplyDelete
  4. Anonymous12:24 PM

    Nifty impressive....The more of ruby I see the less I like. The reason? Its less and less like a language that the customer can understand...

    I.e encourages you to program like old C developers who put nifty point of pointer routines in place.

    These sorts of tricks will be the death of ruby. Much better to have a verbose language that can be read by many and use (e.g. intellij) to autocomplete the "hard" read "not so hard" part of actually coding and supporting an application.

    Perhaps just too stupid to understand...:-)

    ReplyDelete
  5. Jay the title looks a little weird...
    Ruby: Invoking a method with a ¶meter (I think your blog software replaced the ampersand;para with the special char for new paragraph)

    Sudhindra

    ReplyDelete
  6. Sudindra,
    Thanks for the note. Strangely, I see the paragraph symbol in your comment, but the title of the post still looks fine to me. Oh well. =)

    ReplyDelete
  7. aahaa.. Its an IE problem. The paragraph symbol shows up in the title when the page is pulled up in IE.. so there you go..

    ReplyDelete

  8. As of right now the %w[cat dog].collect &:upcase syntax is less readable, and thus undesirable. However, if enough people use it, it becomes an idiom and then it is preferred.


    Moreover, as a beginner-to-intermediate Rubyist I can't help wondering if the Ruby community subconsciously likes the more obscure syntax because it helps stratify the knowledgeable from the less-knowledgeable. I don't think anyone's consciously promoting "job security through obscurity", but I think coder machismo probably underlies a great deal of it.

    And I agree with anonymous that it really cuts off all our noses if it slows the adoption of Ruby. Just because you can use Ruby in a million wonderfully expressive ways doesn't mean you always should. But in happier news, at least Ruby golf hasn't taken hold like it has for another scripting language.

    Finally, AFAIK this blog theme lists times on the comments but not dates. Or am I just blind?

    ReplyDelete

Note: Only a member of this blog may post a comment.