Enum.reduce with &-/2 produces unexpected results

I’m going through hexdocs and learning the fundamentals.
I am getting the opposite result of what I would expect and I wondered if someone had an idea of what is happening here?

Enum.reduce(1..4, 0, &-/2) # Expected -10, but observed 2

My understanding is that &-/2 shorthand capture should be fn x, y -> x - y end but this is producing fn x, y -> y - x end?

If I do this long hand it works correctly:

  def reduce_minus(elem, acc) do
    Enum.reduce(elem, acc, fn elem, acc -> acc - elem end)
  end

IO.puts(Enums.reduce_minus(1..4, 0)) # Result is -10

If I use the + operator (&+/2) this works as expected:

Enum.reduce(1..4, 0, &+/2) # Result is 10 as expected

Thanks

&-2/ is &1 - &2, not &2 - &1.

In Elixir the accumulator is given as the second argument to reduce, as you have seen in your second example.

You expect it to be fn elem, acc -> acc - elem end but as you can see you have flipped the order of arguments. If you pass fn elem, acc -> elem - acc end as &-/2 would do, you will have those operations:

1 - 0 -> 1
2 - 1 -> 1
3 - 1 -> 2
4 - 2 -> 2
3 Likes

I copy-pasted the wrong version. Let me simplify with iex:

iex(1)> Enum.reduce(1..4, 0, &+/2)
10
iex(2)> Enum.reduce(1..4, 0, &-/2)
2

Shouldn’t the latter with &-/2 be -10?

Can you show step by step why you think it should be -10?

The accumulator is the 2nd arg.

So you are doing

Let f(x, y) = x - y

f(4, f(3, f(2, f(1, 0))))
= 4 - (3 - (2 - (1 - 0)))
= 4 - (3 - (2 - 1))
= 4 - (3 - 1)
= 4 - 2
= 2

3 Likes

Thanks. That makes sense now.

I wanted to add to this post, to highlight potential issues with LLMs. I’m not a big fan of blindly using LLMs, but if I come across something I haven’t seen or don’t understand, LLMs can be helpful to give verbose answers.

Here, my initial thought was that if Enum.reduce(1..4, 0, &+/2) # = 10 then the opposite would be true if the negative operator was used. That mistake is on me, but all the major LLMs agreed that the answer was indeed, -10 when I queried it. With GPT, I went around in circles for quite some time, with it showing multiple incorrect examples and even telling me to check my Elixir and Erlang versions and installation. It even corrected my longhand version, switching the elem and acc to match the answer.

As a new user, I cannot add screenshots, but here is some sample output from Claude:

Let me break this down step by step.

This is an Elixir expression that uses `Enum.reduce/3` to apply the subtraction operator (`-/2`) over the range `1..4` starting with an initial value of `0`. Let's see how it evaluates:

1. Initial value: 0
2. First step: 0 - 1 = -1
3. Second step: -1 - 2 = -3 
4. Third step: -3 - 3 = -6
5. Fourth step: -6 - 4 = -10

So the final result is -10.

The `-/2` is the binary subtraction operator being used as a function. The `/2` indicates it takes 2 arguments. Each number in the range is subtracted from the accumulated result.

Would you like me to explain more about how reduce functions work in Elixir?

An interesting quirk. I am curious to see if the LLMs are generally less accurate with functional programming now.

1 Like

In the end LLMs are still just big pattern detectors and reappliers. The fact that many programming languages use acc, item might be enough of a signal to drown the fact that elixir doesn’t order parameters on the reduce callback that way.

I just tried this with claude:

The first verbose form:
Enum.reduce(1..4, 0, fn x, acc -> acc - x end)

matches your input of:
Enum.reduce(1..4, 0, &-/2)
Explain the following elixir notation: &-/2

In Elixir, `&-/2` is a shorthand notation that represents the capture operator (`&`) for the subtraction operator (`-`) with arity 2.

Let's break it down: […]

This notation creates an anonymous function that performs subtraction. It's equivalent to writing:

fn(a, b) -> a - b end

Therefore don’t expect LLM output to be correct, expect it to be plausible.

4 Likes

In this specific case, the problem is that Elixir (and Erlang) chose the opposite signature for the reducer function:

Elixir / Erlang: (element, accumulator) -> accumulator

Haskell / Clojure / JS: (accumulator, element) -> accumulator

The explanation the LLM generates works for languages with the second form.

My wild guess about “why is Erlang like that” is the very common idiom of tail-recursive functions accumulating results in the last positional argument :man_shrugging:

1 Like

Just wanted to point out that the initial thinking of “if reducing with +/2 over some list produces n, -/2 will produce -n” is incorrect because of the properties of addition and subtraction. Addition is commutative and associative, and subtraction is not commutative (it’s anti-commutative) and not associative.

So the behavior of addition is actually happening in the “wrong” order for addition as well, you just don’t see it because it doesn’t matter.

5 Likes