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?
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.