In Ruby, how do inject's symbol, block and initial-value forms work, and why does `sales.inject { |sum, s| sum + s.amount }` fail?
answer
- reduce is the same method
- no initial: first element is memo
- empty without initial returns nil
- inject(0, :+) or inject(0) { }
- memo first, element second
basics
~20 sWithout an initial value, inject uses the first element as the memo, so the first call computes Sale + Integer and raises NoMethodError. inject(0) { ... } fixes it; inject(:+) and inject(0, :+) name a method instead of a block.
solid answer
~40 s`inject`, alias `reduce`, threads a memo through the collection: each call receives `|memo, element|` and returns the next memo. It has three forms: a **block**, `inject { |m, x| ... }`; a **symbol**, `inject(:+)`, which calls that method on the memo with each element; and either form with an **initial value** first, `inject(0) { ... }` or `inject(0, :+)`. Without an initial value, the **first element becomes the memo** and the block starts at the second, so `sales.inject { |sum, s| sum + s.amount }` computes `Sale + Integer` on its first call and raises `NoMethodError`. A single-element collection returns that element without calling the block, and an empty one returns `nil`, so `[].inject(:+)` is `nil` while `[].inject(0, :+)` is `0`. For plain totals, `sum` is shorter.
code
ruby · 13 linesSale = Data.define(:region, :amount)
sales = [Sale.new(region: "north", amount: 300), Sale.new(region: "south", amount: 120)]
sales.inject { |sum, s| sum + s.amount }
# NoMethodError: undefined method '+' for an instance of Sale
sales.inject(0) { |sum, s| sum + s.amount } # => 420
sales.map(&:amount).inject(:+) # => 420
sales.sum(&:amount) # => 420
[].inject(:+) # => nil
[].inject(0, :+) # => 0
[sales.first].inject { |a, b| raise "never called" } # => the Sale itselfgo deeper
Recall that inject and reduce are the same method and that the block receives the memo and then the element.
Explain the four forms, why a missing initial value makes the first element the memo, and what an empty collection returns.
Require an initial value wherever types differ or the list can be empty, and replace numeric injects with sum in review.
Prefer named reductions such as sum, tally or group_by over general inject in shared code, reserving inject for domain-specific combinations.
## What `inject` does `inject`, also called `reduce`, collapses a collection into one value by threading an **accumulator**, the *memo*, through it. For each element it calls the block with `memo, element`, and the block's return value becomes the memo for the next element. The last memo is the result. For a franchise report, totalling the regions' sales: ```ruby region_totals.inject(0) { |total, amount| total + amount } ``` ## The forms `inject` accepts four combinations of arguments: | Call | Memo starts as | Step | |---|---|---| | `inject { \|m, x\| ... }` | the first element | the block | | `inject(init) { \|m, x\| ... }` | `init` | the block | | `inject(:sym)` | the first element | `memo.sym(element)` | | `inject(init, :sym)` | `init` | `memo.sym(element)` | The symbol form names a method that is called **on the memo** with the element as its argument, so `inject(:+)` computes `((e1 + e2) + e3) ...`. Calling `inject` with no block and no argument raises `ArgumentError`. ## When there is no initial value Without an initial value, Ruby uses the **first element as the starting memo** and calls the block from the second element onwards. That shortcut is convenient for numbers and dangerous for everything else: 1. `sales.inject { |sum, s| sum + s.amount }` starts with `sum` set to the first **Sale object**. 2. The first call evaluates `Sale + Integer`, and `Sale` has no `+`, so Ruby raises `NoMethodError`. 3. With exactly one sale, the block is never called at all, and `inject` returns the `Sale` itself instead of a number. 4. With no sales, it returns **`nil`**. The rdoc in Ruby's `enum.c` spells out the condition: the shortcut can only be used when the block produces a result that can be passed back to it as its first parameter. When elements and result have different types, pass an initial value. ## Empty collections The empty case deserves its own line in any answer, because it silently changes a type: - `[].inject(:+)` returns `nil`. - `[].inject(0, :+)` returns `0`. - `[].sum` returns `0`. A report that formats `nil` as a currency amount fails far from the cause, so totals that may be empty always get an initial value. ## Block, symbol or `sum`? - For **numeric totals**, `sum` or `sum { |s| s.amount }` is shorter, returns `0` for an empty list, and handles floats more accurately. - For **other binary operations** named by a method, the symbol form reads well: `inject(:*)` for a product, `inject(:merge)` for combining hashes, `inject(:&)` for intersecting sets. - For **anything with logic**, the block form with an explicit initial value is the clearest. ## Parameter order and mistakes - The memo is the **first** block parameter and the element the second. Swapping them compiles and runs, then fails or produces nonsense on the first call. - Passing both a symbol and a block uses the symbol; the block is ignored, with a warning when warnings are verbose. - `inject(:sym)` with a block is read as an initial value of `:sym`, which is almost never intended. ## Reading an `inject` in review A short checklist catches most `inject` bugs before they ship: 1. **Is there an initial value?** If the memo's type differs from the elements', or the list can be empty, there must be one. 2. **Does the block's last expression return the memo?** An assignment, a `puts` or a conditional that can yield `nil` on its last line hands the wrong value forward. 3. **Are the parameters in memo-then-element order?** 4. **Is there a named method that says it better?** `sum`, `tally`, `group_by`, `min`, `max` and `to_h` each replace a common hand-written `inject` and are easier to read. The fourth question is often the most valuable: an `inject` that survives it usually expresses something domain-specific, which is exactly where a general fold earns its place. ## What interviewers listen for Name the four forms, then the no-initial-value rule, with its three symptoms: the type error on the first call, the single-element case returning an element, and `nil` for an empty collection. Recommending `sum` for totals shows you know when not to reach for `inject` at all.
- What does inject return for a one-element collection with no initial value?The element itself, without calling the block. With no initial value, the first element becomes the memo and there is nothing left to combine it with. That hides type bugs: `[sale].inject { |sum, s| sum + s.amount }` returns the Sale object rather than raising, so the failure appears only when a second sale arrives.
- How does the symbol form decide what to call?It calls the named method on the current memo with each element as the argument: `inject(:+)` computes `memo + element` at every step. The method must exist on the memo's class and return a value that also responds to it, which is why `inject(:merge)` works for a list of hashes.
- Why is sum usually better than inject(:+) for numbers?`sum` starts at 0, so an empty collection returns 0 rather than `nil`, accepts a block for mapping, and on floats uses compensated summation, which reduces rounding error. It also avoids a method dispatch per element for common numeric types.
saying these in an interview costs you the question
- inject without an initial value starts the memo at 0
- [].inject(:+) returns 0
- The element comes first and the memo second in inject's block
- inject(:+) calls + on the element with the memo as argument
- A one-element collection still calls inject's block once