In Ruby, why does puts [1, 2, 3].map do |n| n * 2 end print an Enumerator instead of the doubled numbers?
answer
- two block syntaxes, one difference
- braces bind to the nearest call
- do...end binds to the outer call
- blockless map returns an Enumerator
- parentheses settle the binding
basics
~20 sdo...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 sRuby 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 linesputs [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
# 6go deeper
Recall that braces bind to the nearest call and do...end to the outer call, and that parentheses remove the ambiguity.
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.
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.
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