In Ruby, why does `rows.inject({}) { |h, r| h[r.bib] = r.secs }` fail, and how does each_with_object avoid it?
answer
- the block's value becomes the memo
- assignment evaluates to the value
- each_with_object ignores the block's value
- parameter order is swapped
- needs a mutable memo
basics
~20 sinject passes the block's return value on as the next memo, and h[k] = v evaluates to v, so the hash is lost after the first row. each_with_object passes the same object every time, ignores the block's value and returns that object.
solid answer
~40 sWith `inject`, whatever the block returns becomes the accumulator for the next element. An element assignment such as `h[r.bib] = r.secs` evaluates to `r.secs`, so on the second row `h` is an Integer and `h[...] =` raises `NoMethodError`; with a single row the method silently returns a number instead of a hash. The inject fix is to end the block with `h`. `each_with_object({}) { |r, h| h[r.bib] = r.secs }` avoids the problem by design: it hands the **same** object to every call, ignores the block's value and returns that object. Two details matter: its block parameters are `|element, memo|`, the reverse of inject's `|memo, element|`, and the memo must be mutable, since `each_with_object(0) { |r, sum| sum += r.secs }` returns 0.
code
ruby · 17 linesRow = Data.define(:bib, :secs)
rows = [Row.new(bib: 101, secs: 7530), Row.new(bib: 205, secs: 7612)]
rows.inject({}) { |h, r| h[r.bib] = r.secs }
# NoMethodError: undefined method '[]=' for an instance of Integer
rows.first(1).inject({}) { |h, r| h[r.bib] = r.secs }
# => 7530, a number, not a hash
rows.inject({}) { |h, r| h[r.bib] = r.secs; h }
# => {101 => 7530, 205 => 7612}
rows.each_with_object({}) { |r, h| h[r.bib] = r.secs }
# => {101 => 7530, 205 => 7612}
rows.each_with_object(0) { |r, sum| sum += r.secs }
# => 0go deeper
Recall that inject's next memo is whatever the block returns, while each_with_object returns the object you passed in.
Explain why h[k] = v breaks inject, the reversed parameter order of each_with_object, and why an Integer memo does not accumulate there.
Recognise the single-element case that returns a number instead of a hash, and prefer the method that makes the accumulator's shape obvious in review.
Set a readable convention: inject for values that are replaced, each_with_object for containers that are filled, and dedicated methods where a common shape exists.
## The task A marathon results import has one row per runner, and the next step needs a lookup **Hash** from bib number to finishing time in seconds. Both `inject` and `each_with_object` can build it, but they pass their accumulator in different ways, and that difference is the whole question. ## How `inject` threads its memo `inject` (also called `reduce`) keeps an **accumulator**, often called the memo. On each element it calls the block with `memo, element`, and the block's **return value becomes the memo** for the next element. The final memo is the result. That is exactly right for arithmetic, where the block naturally returns the new value. It goes wrong when the block's last expression is an assignment: ```ruby rows.inject({}) { |h, r| h[r.bib] = r.secs } ``` In Ruby an element assignment `h[k] = v` **evaluates to `v`**, not to the hash. So: 1. First row: `h` is `{}`, the block stores one entry and returns `r.secs`, an Integer. 2. Second row: `h` is now that Integer, and `Integer` has no `[]=` method, so Ruby raises `NoMethodError`. 3. With exactly **one** row there is no second call, and the method returns a bare number instead of a hash. That is the worse outcome, because nothing fails until much later. The inject-shaped fix is to return the hash explicitly: `{ |h, r| h[r.bib] = r.secs; h }`. Using `h.merge(r.bib => r.secs)` also works but builds a new hash on every step. ## How `each_with_object` threads its memo `each_with_object(obj)` passes the **same object** to every call and **ignores** the block's return value. When the walk ends it returns `obj`. The rdoc signature in Ruby's `enum.c` shows it: `each_with_object(object) { |(*args), memo_object| ... } -> object`. ```ruby rows.each_with_object({}) { |r, h| h[r.bib] = r.secs } ``` No trailing `h` is needed, and a single row, or none, still returns a hash. ## Side by side | | `inject(memo)` | `each_with_object(memo)` | |---|---|---| | Block parameters | `\|memo, element\|` | `\|element, memo\|` | | Next memo is | the block's return value | the same object again | | Returns | the last memo | the object you passed in | | Works with immutable memo | yes (numbers, frozen strings) | no, rebinding is lost | | Natural use | combining values into one value | filling a hash or array | ## The two traps around `each_with_object` - **Parameter order.** The element comes first and the memo second, the reverse of `inject`. Swapping them in the block does not raise; it just stores rows under the wrong keys. - **Immutable memos.** `each_with_object(0) { |r, sum| sum += r.secs }` returns `0`. The `+=` rebinds the block-local variable `sum` to a new Integer; the object held by `each_with_object` never changes. Integers are immutable, so there is nothing to mutate. Totals belong to `inject`, `sum` or similar. ## Choosing between them A practical rule reviewers use: - If the accumulator is a **value** you replace (a number, a frozen string, a new record each step), use `inject`. - If the accumulator is a **container** you fill in place (a hash, an array, a set), use `each_with_object`. - If the shape is a common one, such as pairs into a hash, grouping, or counting, a dedicated method usually reads better than either. `Enumerator#with_object` is the same operation reached through an enumerator, so `rows.each_with_index.with_object({})` can combine an index with a memo. ## Other memo types The same distinction applies to arrays and sets, not just hashes: - `rows.each_with_object([]) { |r, acc| acc << r.bib if r.secs < 10_800 }` fills an array in place. Written with `inject`, the same block would hand `nil` on as the next memo whenever the `if` modifier is false, because a failed modifier `if` evaluates to `nil`; the bug appears only for some rows, which makes it easy to miss. - `rows.each_with_object(Set.new) { |r, seen| seen << r.country }` collects distinct values; `Set` is a core class in Ruby 4.0, so no `require` is needed. - A **frozen** memo, such as an array created with `.freeze`, raises `FrozenError` as soon as the block tries to append to it, because the method never replaces the object. In every case the question to ask is the same: does the block **change the object** it was given, or does it **produce a new one**? Change means `each_with_object`; produce means `inject`. ## What interviewers listen for The strong answer names the mechanism, not just the fix: inject's next memo is the block's value, assignment returns the assigned value, and each_with_object sidesteps that by returning the object it was given. Then mention the swapped parameter order and the immutable-memo trap, which show you have actually used it.
- Why does each_with_object(0) with sum += n return 0?`each_with_object` keeps a reference to the object you passed and hands that same object to every call. `sum += n` does not change that object; it rebinds the block's local `sum` to a new Integer, which is discarded when the block ends. Integers are immutable, so nothing reaches the memo, and the original 0 is returned.
- When is inject still the better choice for building something?When each step produces a new value rather than filling a container: a running total, a frozen string, or an immutable record replaced at every step. `inject` threads the returned value forward, which is exactly what those need, while `each_with_object` would keep handing back the unchanged starting object.
- How do you combine each_with_object with an index?Chain through an enumerator: `rows.each_with_index.with_object({}) { |(row, i), h| h[i + 1] = row.bib }`. `Enumerator#with_object` is the same operation as `each_with_object`, and the element it yields is the `[row, index]` pair, so the parentheses destructure it.
inject is a relay baton: each runner must hand back whatever they are holding, and if someone hands back a water cup instead, the next runner gets the cup. each_with_object is a clipboard bolted to the table: every runner writes on the same one, and it is the clipboard that is collected at the end.
saying these in an interview costs you the question
- inject and each_with_object take their block parameters in the same order
- each_with_object returns the value of the block's last expression
- each_with_object(0) with sum += n accumulates a running total
- An assignment h[k] = v returns the hash, so inject keeps the hash
- inject over a single row still returns a hash, so the bug always raises