Sunday, February 15, 2015

Ruby blocks with brackets are parsed differently

In Ruby, you can write blocks using either brackets or with the do/end keywords. Check out this code that gives surprising results:


a = []
 
a.concat a.map { |b| }  # => []
 
a.concat a.map do |b|
end
# => no implicit conversion of Enumerator into Array (TypeError)

It turns out that the Ruby parser actually cares which notation you use for your blocks, and that affects which method will receive the block. In the code above, a block written with brackets gets passed to the last method in the statement. A block written with do/end gets passed to the first method, which causes the code example above to break.

You can even use both types of blocks in the same statement, and they go to different methods:


def f(x)
  puts 'f got block' if block_given?
end
 
def g(x)
  puts 'g got block' if block_given?
end
 
def h
  puts 'h got block' if block_given?
end
 
f g h { } do
end
# f got block
# h got block

I used to think that the different notations for blocks were just equivalent ways of writing the same thing, and I said that do/end should be used if and only if the block is multi-line. Now it seems like there might be legitimate reasons to break that rule, or to come up with a different rule.

2 comments:

  1. Thanks for this! I ran into this and your blog helped me get it working. I tracked down what's going on...

    # As you state, this fails:
    a = []
    a.concat a.map do |b|
    end

    # The reason is precedence ({} blocks are lowest precedence.)
    # This is effectively what's happening:
    a.concat(a.map) do |b|
    end

    # This isn't pretty, but it works by using parenthesis to clarify intention
    a.concat (a.map do |b|
    end)

    ReplyDelete
    Replies
    1. Hey, I'm glad you found my post to be useful!

      Delete