Why is the capture syntax &+/2 equivalent to &(&1 + &2)

I don’t understand how the capture syntax in the following expression is aware of how to use arguments without specifying &1 and &2:
Enum.reduce([1, 2, 3], 0, &+/2)

I understand this version of an equivalent expression since it is explicit how the arguments &1 and &2 are used in the function:
Enum.reduce([1, 2, 3], 0, &(&1 + &2))

4 Likes

It simply takes advantage of the positional function parameters.

+/2 is Kernel.+/2, i.e. Kernel.+(left, right) is equivalent to left + right.

The & operator captures the function rather than evaluate it - so it’s &Kernel.+/2 or &+/2.

Now look at the spec for Enum.reduce/3

reduce(t, any, (element, any -> any)) :: any

(element, any -> any) tells us that it takes a two parameter function. The first parameter (element) comes from the enumeration, the second parameter (any) is the accumulator which is the same type as the function’s result.

So the element (first) argument goes into the left parameter and the any (second) argument goes into right parameter of Kernel.+/2 (… and the result becomes the next right and so on).

6 Likes

FWIW, I would cautious about using this construct, there are a couple of weird edge cases in which Kernel.+ works because it has both and unary and binary version and the parser special cases some code in that case.

This generally comes up when you find some trick that works with + but does not with *.
The underlying reason is that * does not have a unary version.

I hadn’t considered that +/2 was referencing a function form the kernel module but it makes sense now why both the capture syntaxes I cited get the same result.

Is there a special name or property that references the fact that I can’t call iex> +(1, 2) but I can call iex> 1 + 2
And that I can call iex> Kernel.+(1, 2) but not iex> 1 Kernel.+ 2

Put more generally, why does &Kernel.+/2 seem to behave differently than &+/2?

Let’s start with some iex

iex(1)> h Kernel.+/1
def +(value)
Arithmetic unary plus.
Allowed in guard tests. Inlined by the compiler.
Examples
┃ iex> +1
┃ 1
iex(2)> h Kernel.+/2
def +(left, right)
Arithmetic addition.
Allowed in guard tests. Inlined by the compiler.
Examples
┃ iex> 1 + 2
┃ 3

The important thing to remember with Elixir is that no matter what the syntax looks like, it all gets turned into function calls. Every time you use + it gets turned into one of those two functions.

iex(3)> quote do: 1 + 2
{:+, [context: Elixir, import: Kernel], [1, 2]}
iex(4)> quote do: + 1
{:+, [context: Elixir, import: Kernel], [1]}

So let’s look at what the parser sees in each case.

iex> +(1, 2)

In this case the parser looks to somehow call Kernel.+1/ but it’s given an arg list that
is two elements long. This generates an syntax error.

iex> 1 Kernel.+ 2

Here the parser sees a number so it starts looking for an infix operator it can turn into a function call. While + is defined as an infix operator, arbitrary elixir functions cannot be used
as infix operators. So again it generates a syntax error.

What is going on is that the parser is attempting to be as friendly as possible and allow you to use constructs you are familiar with like 1 + 2. However, that can only be extended so far before it starts to break the underlying model of a functional programming language. + is particularly tricky since it exists in the parser as both a prefix and infix operator.

The effects you see are the attempts of the parser to understand + as both. Once you
put Kernel.+ into the equation all the special parsing rules are turned off, and it behaves like any other Elixir function.

4 Likes

Thanks for the elaborate response. This has made things much clear for me :relaxed: