Search code examples
rubyreduce

Inject behavior when last element is used before referencing accumulator


I am a a bit confused by some of the behavior of the of the inject (alias reduce) command in Ruby

Background

For any array, the second element (referenced after the accumulator) reflects the last element in the array so
[1,2,10].inject{|accumulator,y| y} gives 10

whereas the first element is the initial value of the array (that subsequently gets the result of any specified operation) if not otherwise specified so
[1,2,10].inject{|accumulator,y| accumulator} gives 1

Addition

This results in the expected behavior for
[1,2,10].inject{|accumulator,y| accumulator + y} gives 13
OR
[1,2,10].inject{|accumulator,y| y + accumulator} gives 13

However,

If a string is used
["b","i","g"].inject{|x,y| y + x} the result is "gib".
Why is it not "gbi"?
I would expect (last element + first element)+ i OR ("g"+"b")+ "i"

Subtraction

It also works as expected for subtraction when x is the value to be acted on first
[1,2,10].inject{|x,y| x - y} gives -11 i.e (1-2) - 10 = -11 as expected

However,

when 'y' is the first element to be acted on

[1,2,10].inject{|x,y| y - x} gives 9.
This is surprising. Only the last element and the first are used i.e 10-1.
Why is there is no accumulated result?!


Solution

  • For any array, the second element (referenced after the accumulator) reflects the last element in the array

    This incorrect statement is the source of the misunderstanding.

    Look at the following code:

    ["b", "i", "g", "g", "e", "s", "t"].inject do |x, y|
      puts({ y: y, x: x, "y + x": (y + x) })
      y + x
    end
    

    which prints

    {y: "i", x: "b", "y + x": "ib"}
    {y: "g", x: "ib", "y + x": "gib"}
    {y: "g", x: "gib", "y + x": "ggib"}
    {y: "e", x: "ggib", "y + x": "eggib"}
    {y: "s", x: "eggib", "y + x": "seggib"}
    {y: "t", x: "seggib", "y + x": "tseggib"}
    

    As you can see, the second block parameter isn't the last element in the array, it's the next element in the array. ["b", "i", "g"].inject uses "b" as the first accumulator and passes "i", then "g".


    Only the last element and the first are used i.e 10-1. Why is there is no accumulated result?!

    This statement exhibits the same misunderstanding as above. The return value is 9 not because it's "last minus first" (10 - 1) but because it's "last minus the result of second minus first" (10 - (2 - 1)). The resulting values just happen to be the same in your example.

    Here's an illustration of how it actually works, using a different array:

    [1, 4, 9, 16, 25].inject do |x, y|
      puts({ y: y, x: x, "y - x": (y - x) })
      y - x
    end
    

    This prints

    {y: 4, x: 1, "y - x": 3}
    {y: 9, x: 3, "y - x": 6}
    {y: 16, x: 6, "y - x": 10}
    {y: 25, x: 10, "y - x": 15}
    

    At each step after the first, the value of x is set to the result of y - x in the previous step. So, there is, in fact, an accumulated result, regardless of the contents of the array and regardless of which operation is used.