Search code examples
rubysyntaxlambdablock

Does Ruby read "do" and "end" the same way as "{" and "}"?


There are two different syntax for writing blocks in Ruby. There's

do |something|
    ...
end

and there's also

{ |something|
    ...
}

I realized that what this is doing is just replacing the keywords "do" and "end" with brackets. This made me wonder, when Ruby works with these two syntax, is there any significant difference in the way it handles them. Or does Ruby handle them the same way, interchangeably? If they're totally interchangeable, then why include both?


Solution

  • No, it doesn't. As a general rule of Ruby, if two things look alike, you can bet that there is a subtle difference between them, which makes each of them unique and necessary.

    { and } do not always stand in the role of block delimiters. When they do not stand in the role of block delimiters (such as when constructing hashes, { a: 1, b: 2 }), they cannot be replaced by do ... end. But when the curly braces do delimit a block, they can almost always be replaced by do ... end. But beware, because sometimes this can change the meaning of your statement. This is because { ... } have higher precedence, they bind tighter than do ... end:

    puts [ 1, 2, 3 ].map { |e| e + 1 }         # { ... } bind with #map method
    2
    3
    4
    #=> nil
    
    puts [ 1, 2, 3 ].map do |e| e + 1 end      # do ... end bind with #puts method
    #<Enumerator:0x0000010a06d140>
    #=> nil
    

    As for the opposite situation, do ... end in the role of block delimiters cannot always be replaced by curly braces.

    [ 1, 2, 3 ].each_with_object [] do |e, obj| obj << e.to_s end  # is possible
    [ 1, 2, 3 ].each_with_object [] { |e, obj| obj << e.to_s }   # invalid syntax
    

    In this case, you would have to parenthesize the ordered argument:

    [ 1, 2, 3 ].each_with_object( [] ) { |e, obj| obj << e.to_s }  # valid with ( )
    

    The consequence of these syntactic rules is that you can write this:

    [ 1, 2, 3 ].each_with_object [ nil ].map { 42 } do |e, o| o << e end
    #=> [ 42, 1, 2, 3 ]
    

    And the above also demonstrates the case where {} are not replaceable by do/end:

    [ 1, 2, 3 ].each_with_object [ nil ].map do 42 end do |e, o| o << e end
    #=> SyntaxError
    

    Lambda syntax with -> differs slightly in that it equally accepts both:

    foo = -> x do x + 42 end  # valid
    foo = -> x { x + 42 }     # also valid
    

    Furthermore, do and end themselves do not always delimit a block. In particular, this

    for x in [ 1, 2, 3 ] do
      puts x
    end
    

    and this

    x = 3
    while x > 0 do
      x -= 1
    end
    

    and this

    x = 3
    until x == 0 do
      x -= 1
    end
    

    superficially contains do ... end keywords, but there is no real block between them. The code inside them does not introduce a new scope, it is just syntax used to repeat a few statements. Curly braces cannot be used here, and the syntax could be written without do, such as:

    for x in [ 1, 2, 3 ]
      puts x
    end
    
    # same for while and until
    

    For the cases where {...} and do...end delimiters are interchangeable, the choice which ones to use is a matter of programming style, and is extensively discussed in another question (from whose accepted answer I took one of my examples here). My personal preference in such case is to emphasize readability over rigid rules.