differing `puts` behavior inside `do..end` blocks and curly braces in Ruby

Viewed 67

I'm an experienced developer but a Ruby newbie. I was reading this thread do..end vs curly braces for blocks in Ruby and learned some good stuff about when to use braces and when to use do .. end blocks. One of the tips was use do .. end when you are after side effects and {} when you are concerned about the return value. So experimenting with some of the examples in the enumerator tutorial I'm going through:

irb(main):001:0> my_array = [1, 2]
=> [1, 2]
irb(main):002:0> my_array.each {|num| num *= 2; puts "The new number is #{num}."}
The new number is 2.
The new number is 4.
=> [1, 2]
irb(main):003:0> my_array.each do |num| num *= 2; puts "The new number is #{num}." end
The new number is 2.
The new number is 4.
=> [1, 2]

Hang on. I thought the do..end block returns an enumerator object? It looks like an array. Let's check:

irb(main):004:0> puts my_array.each {|num| num *= 2; puts "The new number is #{num}."}
The new number is 2.
The new number is 4.
1
2
=> nil
irb(main):005:0> puts my_array.each do |num| num *= 2; puts "The new number is #{num}." end
#<Enumerator:0x000055967e53ac40>
=> nil

Okay, it is an enumerator. But what happened to the output of the puts call in the loop on line 005? The {} had the expected side effect but not the do..end block, which seems to violate that rule of thumb.

What happened to my "The new number is #{num}." strings?

1 Answers

do...end and {} are 100% semantically equivalent for method blocks. Their only difference is their parsing precedence, so they evaluate differently

To really understand that difference, a few things first.

Ruby lets you call methods without parens:

my_object.my_method my_arg

# so my_arg could actually be a method! Let's put parens in to show that:
my_object.my_method(my_arg())

Blocks in Ruby are method arguments -- a syntax for passing in closures (except for the special keywords that act in the parent scope). The below two chunks are equivalent:

[1, 2, 3].map { |x| 2 * x }

# split out into two lines
double = ->(x) { 2 * x }  # shorthand for `lambda { |x| 2 * x }`
[1, 2, 3].map(&double)

Okay, so knowing all that, let's expose the difference between {} and do...end:

my_method [1, 2, 3].map { |x| 2 * x }

my_method([1, 2, 3].map { |x| 2 * x }) # parses like this


my_method [1, 2, 3].map do |x| 2 * x end

my_method([1, 2, 3].map) do |x| 2 * x end # parses like this

my_method([1, 2, 3].map) { |x| 2 * x }    # in other words

{} has more precedence than do...end, immediately getting associated to the method immediately to its left. do...end has lower precedence, and will associate with my_method, which gets passed [1, 2, 3].map and the block as arguments.

This means, what you did above is:

puts(my_array.each) { |num| num *= 2; puts "The new number is #{num}." }

You've passed into puts the my_array.each, which is an enumerator, and a block, and puts does nothing with the blocks passed into it, as do all methods by default.

Related