skip to content

In Ruby, why does puts [1, 2, 3].map do |n| n * 2 end print an Enumerator instead of the doubled numbers?

level: middleimportance: must knowfreq 55%

answer

  1. two block syntaxes, one difference
  2. braces bind to the nearest call
  3. do...end binds to the outer call
  4. blockless map returns an Enumerator
  5. parentheses settle the binding

basics

~20 s

do...end binds more loosely than braces, so in a call without parentheses the block goes to puts, not map. map runs without a block and returns an Enumerator, and puts prints that object and ignores the block.

solid answer

~50 s

Ruby has two block syntaxes that differ only in precedence. `{ }` binds to the nearest method call on its left, so `puts [1, 2, 3].map { |n| n * 2 }` gives the block to `map` and prints 2, 4 and 6. `do ... end` has lower precedence, so in `puts [1, 2, 3].map do |n| n * 2 end` the block attaches to `puts`, the outermost call. `map` then runs without a block and returns an `Enumerator`, and `puts` ignores the block it was given and prints the enumerator's default `to_s`, something like `#<Enumerator:0x...>`. Fixes: use braces, wrap the argument in parentheses as `puts([1, 2, 3].map do |n| n * 2 end)`, or assign the result first. The common convention is braces for one-line blocks whose value you use and `do ... end` for multi-line blocks.

code

ruby · 12 lines
ruby
puts [1, 2, 3].map { |n| n * 2 }
# 2
# 4
# 6

puts [1, 2, 3].map do |n| n * 2 end
# #<Enumerator:0x...>   (the block went to puts)

puts([1, 2, 3].map do |n| n * 2 end)
# 2
# 4
# 6

go deeper

for a junior

Recall that braces bind to the nearest call and do...end to the outer call, and that parentheses remove the ambiguity.

for a middle

Trace the puts example step by step: the block goes to puts, map runs blockless and returns an Enumerator, and puts prints its to_s.

for a senior

Explain the team conventions that prevent this bug, why a single-call line is never ambiguous, and why the Ruby 3.4 unused-block warning does not catch C methods.

for a principal

Decide how far a team should enforce block-style conventions through review and linting, balancing readability of DSL-heavy code against the cost of silent precedence bugs.

## Two spellings of a block In Ruby a **block** can be written with braces or with `do ... end`: ```ruby [1, 2, 3].each { |n| puts n } [1, 2, 3].each do |n| puts n end ``` When the block is attached to a single call, the two forms are interchangeable. They differ in exactly one respect: **precedence**, meaning which method call a block attaches to when several calls appear on one line without parentheses. ## The binding rule Ruby's calling-methods reference states that `do ... end` has lower precedence than `{ }`: - **Braces bind tightly** to the nearest method call on their left. - **`do ... end` binds loosely** to the outermost call in the expression when that call's arguments are written without parentheses. So in `method_1 method_2 { ... }` the block goes to `method_2`, while in `method_1 method_2 do ... end` the block goes to `method_1`. If the outer call uses parentheses, `method_1(method_2 do ... end)`, the block stays inside them and attaches to `method_2`. ## Tracing the puts example ```ruby puts [1, 2, 3].map do |n| n * 2 end ``` 1. The parser attaches the `do ... end` block to `puts`, because `puts` is the outer call and its argument has no parentheses. 2. `[1, 2, 3].map` therefore runs **without a block**. Called that way, `map` returns an **`Enumerator`** instead of an array. 3. `puts` receives the enumerator as its only argument. `puts` is implemented in C and never runs a block, so the block is silently ignored. 4. The enumerator has no `to_ary`, so `puts` prints its `to_s`, the default object description such as `#<Enumerator:0x...>`. No exception is raised; the output is simply wrong, which is why this bug survives into code review. ## Ways to fix it | Rewrite | Block goes to | Prints | |---|---|---| | `puts [1, 2, 3].map { \|n\| n * 2 }` | `map` | 2, 4, 6 | | `puts([1, 2, 3].map do \|n\| n * 2 end)` | `map` | 2, 4, 6 | | `doubled = [1, 2, 3].map do \|n\| n * 2 end` then `puts doubled` | `map` | 2, 4, 6 | | `puts [1, 2, 3].map do \|n\| n * 2 end` | `puts` | an Enumerator | Assignment is safe because `=` is not a method call competing for the block; the only call on the right-hand side is `map`. ## Spotting it in review The bug has a recognisable shape, so it is worth scanning for: - a method call **without parentheses** whose argument is itself a method call, followed by `do`; - outer calls such as `puts`, `p`, logger calls and assertion helpers, which accept a block and may ignore it; - output or return values that look like `#<Enumerator:...>` where data was expected. When the outer method *does* use a block, the result is worse than wrong output: the outer method runs the block with its own arguments, and the inner call still runs blockless. Adding parentheses to the outer call removes the ambiguity in every case. ## Style conventions The precedence difference is why many Ruby teams follow a simple convention: - Use **braces** for single-line blocks, especially when the block's value is used, as with `map`, `select` or `sum`. - Use **`do ... end`** for multi-line blocks, especially ones run for side effects. - When a `do ... end` block follows an argument list, add **parentheses** to the outer call so the binding is obvious. The trap needs **two calls competing for one block**. A line with a single call, such as `task :deploy do ... end` in a Rakefile, has nothing to compete with, so `do ... end` is safe there. ## Catching it earlier Since Ruby 3.4, running with `-w` can warn that a block passed to a **Ruby-defined** method which never uses a block may be ignored. That check does not cover C-implemented methods such as `puts`, so the example above still runs silently; parentheses and tests remain the real defence.

  • Why does doubled = [1, 2, 3].map do |n| n * 2 end work correctly?
    Assignment is not a method call, so the only call that can take the block is `map`. The block binds to `map`, which returns `[2, 4, 6]`, and that array is assigned to `doubled`. The precedence trap needs two method calls competing for one block.
  • Why does Ruby not raise an error when puts receives a block it does not use?
    Any Ruby method call may carry a block, and a method that never yields or captures it just ignores it. Since Ruby 3.4, `-w` can warn about blocks passed to Ruby-defined methods that never use a block, but C methods such as `puts` are not covered by that check.

saying these in an interview costs you the question

  • Braces and do...end are always interchangeable
  • do...end binds to the method nearest to it, like braces
  • puts raises ArgumentError when it is given a block
  • map without a block returns an empty array
  • Assigning the map result first still sends the block to the wrong method