Skip to content

inject makes the block's value the next accumulator

Level: 201 · anyone who has written inject({}) { |h, x| h[x] = … } and been told an Integer has no []=

One line: inject feeds each block result back in as the accumulator, so a block that ends in an assignment hands the next round a number instead of the hash; each_with_object returns the memo whatever the block returns; and sum is not inject(:+) on floats — Ruby's sum compensates rounding, so [0.1] * 10 sums to 1.0 where inject(:+) gives 0.9999999999999999, and since 3.12 Python's sum does the same while functools.reduce does not.

A fold walks a collection carrying one value along. In inject that value is whatever the block returned last time: the block receives (accumulator, element) and its return value is the next accumulator, which is exactly right for acc + x and exactly wrong for h[x] = v, whose value is v. each_with_object(memo) fixes the shape by handing the same memo object to every call — as (element, memo), note the reversed order — and returning that object at the end, so the block may return anything, but it must mutate the memo rather than rebind it. sum looks like a third spelling of inject(:+) and is not: for Floats the docs say it uses Kahan-Babuska compensated summation, which keeps the rounding error of each addition in a second variable and folds it back in (the algorithm's name is not machine-checked here; its results are, in rows 10–13).

Verified output of inject_and_each_with_object_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

inject (also called reduce)
 1. nums.inject(:+)                                            10
 2. nums.inject(0) { |acc, x| acc + x }                        10
 3. nums.inject(10, :+)  (initial value, then a name)          20
 4. nums.inject(:*)                                            24
 5. reduce(:+); instance_method(:reduce) == (:inject)          [10, true]
 6. [].inject(:+), [].inject(0, :+), [].sum                    [nil, 0, 0]
 7. [7].inject { |a, b| raise }  (one element: no call)        7
 8. words.inject { |best, w| longer of the two }               "banana"
 9. (1..5).inject(:lcm), [12, 18, 24].inject(:gcd)             [60, 6]

sum is not inject(:+): floats are compensated
10. ([0.1] * 10).inject(:+)                                    0.9999999999999999
11. ([0.1] * 10).sum                                           1.0
12. [1e100, 1.0, -1e100].inject(:+), .sum                      [0.0, 1.0]
13. [0.1, 0.2, 0.3].inject(:+), .sum                           [0.6000000000000001, 0.6]
14. [1, Rational(1, 3)].sum, [1, 2].sum(0.0)                   [(4/3), 3.0]

each_with_object: the memo comes back whatever the block returns
15. words.each_with_object({}) { |w, h| h[w] = w.size }        {"apple" => 5, "banana" => 6, "cherry" => 6}
16. words.inject({}) { |h, w| h[w] = w.size; h }  (the ; h)    {"apple" => 5, "banana" => 6, "cherry" => 6}
17. nums.inject({}) { |h, x| h[x] = x * 10 } raises            "NoMethodError: undefined method '[]=' for an instance of Integer"
18. inject([]) { |acc, x| acc << x * 10 }  (<< returns acc)    [10, 20, 30, 40]
19. block args: each_with_object |x, memo|, inject |memo, x|   [[5, "memo"], ["memo", 5]]
20. each_with_object([]) { |w, acc| acc << w; :ignored }       ["apple", "banana", "cherry"]
21. each_with_object("") { |w, acc| acc += w }  (+= rebinds)   ["", "applebananacherry"]
22. each_with_object(Hash.new(0)) { |c, h| h[c] += 1 }         {"b" => 1, "a" => 3, "n" => 2}

Three more folds
23. words.sum(""), words.inject(:+)                            ["applebananacherry", "applebananacherry"]
24. [[1, 2], [3, 4]].inject(:+), .sum([])                      [[1, 2, 3, 4], [1, 2, 3, 4]]
25. each_with_index.inject(0) { |acc, (x, i)| acc + x * i }    20

Reading the output

Rows 1–9, the forms of inject: a symbol names the method to fold with — :+, :*, :lcm, :gcd, any binary method (row 9) — a block spells it out, and an initial value goes first (inject(10, :+), row 3). reduce is not a copy but the same method under a second name: row 5 prints that the two UnboundMethod objects are equal. Without an initial value the first element is the first accumulator, so an empty array folds to nil (row 6, where sum gives 0), and a one-element array returns its element without calling the block at all (row 7 — that block would raise). Row 8 uses the same rule: the fold keeps the longer of the running best and each word.

Rows 10–14, sum versus inject(:+): ten copies of 0.1 add up to 0.9999999999999999 one addition at a time and to 1.0 under sum; 1e100 + 1.0 - 1e100 loses the 1.0 entirely in inject and keeps it in sum (row 12); 0.1 + 0.2 + 0.3 is off by one unit in the last place in inject and exact in sum (row 13). Row 14 is the other half of sum: with non-float elements it is ordinary exact arithmetic (a Rational stays a Rational), and an initial 0.0 makes the result a Float.

Rows 15–22, each_with_object: rows 15 and 16 build the same hash, and the inject version needs the trailing ; h to return the accumulator. Row 17 is what happens without it — the block's value is 10, so 10 becomes the accumulator and the next round calls 10[2] = 20. Row 18 shows why acc << x does not need the trick: << returns its receiver. Row 19 is the second trap, the argument order: each_with_object yields |element, memo| and inject yields |memo, element|. Rows 20–21 are the memo's rule: the block's return value is ignored (:ignored does no harm), but acc += w rebinds the block parameter to a new string and the memo comes back empty, where acc << w mutates it and the memo comes back full. Row 22 is the counting idiom with a default-valued Hash; tally does the same in one word.

Rows 23–25: sum("") and inject(:+) both concatenate strings, inject(:+) and sum([]) both concatenate arrays, and a block can destructure its element with (x, i) when the elements are pairs.

Compared with Python

functools.reduce is inject under the same rules: the function gets (accumulator, element), its return value is the next accumulator, the initial value is an optional third argument, one element comes back without a call (row 7), and an empty iterable with no initial value raises TypeError where Ruby returns nil (row 6). It was a builtin in Python 2 and now lives in functools (Not machine-checked here.) — row 5 prints that builtins has no reduce. The inject({}) trap reproduces exactly in row 17: a function that assigns and returns None makes None the accumulator, and the next round fails with TypeError.

Python has no each_with_object, and does not need one: a for loop has no scope of its own, so sizes[w] = len(w) and even text += w work on the outer variable (rows 15, 20 and 21) — where Ruby's acc += w inside a block rebinds the block parameter and loses the work. Rows 10–13 hold the measured surprise. Python's sum compensates rounding too — the 3.12 release notes introduced it as Neumaier summation (that attribution is not machine-checked here; the values are, under both 3.12 and 3.14) — so sum([0.1] * 10) is 1.0, the same as Ruby's sum, and math.fsum — the older tool, which returns the correctly rounded exact sum — agrees; the naive 0.9999999999999999 now belongs to reduce(operator.add, …) only. Row 14 shows sum staying exact with Fraction, and rows 23–24 the two rules Ruby lacks: sum refuses strings outright ("".join is the answer), and although sum(lists, []) works, itertools.chain.from_iterable is the idiom.

Verified output of inject_and_each_with_object_py.py — regenerated by tools/run_examples.py, never hand-typed.

functools.reduce
 1. reduce(operator.add, nums)                                 10
 2. reduce(lambda acc, x: acc + x, nums, 0)                    10
 3. reduce(operator.add, nums, 10)  (initial value last)       20
 4. reduce(operator.mul, nums)                                 24
 5. one name; hasattr(builtins, "reduce")                      [10, False]
 6. reduce(add, []) raises; reduce(add, [], 0); sum([])        ['TypeError', 0, 0]
 7. reduce(never, [7])  (one element: no call)                 7
 8. reduce(lambda best, w: longer of the two, words)           'banana'
 9. reduce(math.lcm, range(1, 6)), reduce(math.gcd, ...)       [60, 6]

sum is not reduce(add): floats are compensated since 3.12
10. reduce(operator.add, [0.1] * 10)                           0.9999999999999999
11. sum([0.1] * 10), math.fsum([0.1] * 10)                     [1.0, 1.0]
12. reduce(add, [1e100, 1.0, -1e100]), sum(...), fsum(...)     [0.0, 1.0, 1.0]
13. reduce(add, [0.1, 0.2, 0.3]), sum(...)                     [0.6000000000000001, 0.6]
14. sum([1, Fraction(1, 3)]), sum([1, 2], 0.0)                 [Fraction(4, 3), 3.0]

No each_with_object: a loop over an outer variable, or a comprehension
15. sizes = {}; for w in words: sizes[w] = len(w)              {'apple': 5, 'banana': 6, 'cherry': 6}
16. reduce(lambda h, w: h | {w: len(w)}, words, {})            {'apple': 5, 'banana': 6, 'cherry': 6}
17. reduce(assign_only, nums, {}) raises                       'TypeError'
18. reduce(lambda acc, x: acc + [x * 10], nums, [])            [10, 20, 30, 40]
19. reduce's function always gets (acc, x)                     [None, ('memo', 5)]
20. acc = []; for w in words: acc.append(w)                    ['apple', 'banana', 'cherry']
21. text = ""; for w in words: text += w  (kept)               'applebananacherry'
22. dict(Counter("banana"))                                    {'b': 1, 'a': 3, 'n': 2}

Three more folds
23. sum(words, "") raises; reduce(add, words)                  ['TypeError', 'applebananacherry']
24. sum([[1, 2], [3, 4]], []), list(chain.from_iterable(...))  [[1, 2, 3, 4], [1, 2, 3, 4]]
25. sum(x * i for i, x in enumerate(nums)), unpacked in the for 20

Why 0.1 ten times is not 1.0 in the first place is the Python library's Float equality and NaN ↗ page, and the Math library's Machine numbers ↗ explains the rounding that compensated summation undoes.

Try it

  1. Write seven folds with inject or each_with_object and nothing else: the product of 1..6, the longest word in a list, a hash from word to length, the running totals of [3, 1, 4, 1, 5], a reversed copy of [1, 2, 3], the words grouped by first letter (a Hash.new { |h, k| h[k] = [] } as the memo), and [0.1] * 10 summed by inject and then by sum.
  2. Implement compensated summation yourself: inject([0.0, 0.0]) { |(sum, err), x| … } carrying the running sum and the accumulated rounding error, then compare your result with Array#sum on [0.1] * 10 and on [1e100, 1.0, -1e100]. The second one is the hard case: which of the two does your version get right?
Solution to 1

inject_and_each_with_object_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Kata: seven folds, each written with inject or each_with_object and nothing else.
def row(n, label, value) = puts(format("%2d. %-44s %s", n, label, value.inspect))

words = %w[pear fig apple plum kiwi]

row 1, "product of 1..6", (1..6).inject(:*)
row 2, "the longest word", words.inject { |best, w| w.size > best.size ? w : best }
row 3, "word => size, with each_with_object", words.each_with_object({}) { |w, h| h[w] = w.size }
row 4, "running totals of [3, 1, 4, 1, 5]", [3, 1, 4, 1, 5].each_with_object([]) { |x, acc| acc << (acc.last || 0) + x }
row 5, "reverse [1, 2, 3] with inject", [1, 2, 3].inject([]) { |acc, x| [x] + acc }
row 6, "group by first letter, with a default block", words.each_with_object(Hash.new { |h, k| h[k] = [] }) { |w, h| h[w[0]] << w }.to_h
row 7, "[0.1] * 10 by inject, then by sum", [([0.1] * 10).inject(:+), ([0.1] * 10).sum]

Verified output of inject_and_each_with_object_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

 1. product of 1..6                              720
 2. the longest word                             "apple"
 3. word => size, with each_with_object          {"pear" => 4, "fig" => 3, "apple" => 5, "plum" => 4, "kiwi" => 4}
 4. running totals of [3, 1, 4, 1, 5]            [3, 4, 8, 9, 14]
 5. reverse [1, 2, 3] with inject                [3, 2, 1]
 6. group by first letter, with a default block  {"p" => ["pear", "plum"], "f" => ["fig"], "a" => ["apple"], "k" => ["kiwi"]}
 7. [0.1] * 10 by inject, then by sum            [0.9999999999999999, 1.0]

See also