Why do we need a function "capture operator" in Elixir?

Viewed 1604

Can someone explain why, in Elixir, the "capture operator", denoted as an ampersand prefix, is needed? In other languages it isn't :

Python 3.6.0 |Anaconda 4.3.0 (64-bit)| (default, Dec 23 2016, 12:22:00) 
[GCC 4.4.7 20120313 (Red Hat 4.4.7-1)] on linux

>>> def double(x):
...   return(x + x)
... 
>>> double(2)
4
>>> dbl = double
>>> dbl(2)
4

This works the same in Elixir, apparently:

iex(2)> double = fn x -> x + x end
#Function<6.118419387/1 in :erl_eval.expr/5>
iex(3)> double.(2)
4                                                                                                                                 
iex(4)> dbl = double                                                                                                              
#Function<6.118419387/1 in :erl_eval.expr/5>                                                                                      
iex(5)> dbl.(2)                                                                                                                   
4    

So why, for example here, do we ever need to use a capture operator if the function can already be passed around without said operator? Doesn't the plain old name of the function already "capture" it?

iex(10)> Enum.map([1, 2, 3], double)
[2, 4, 6]

Basically I don't understand the use case for the & capture operator and what advantages it affords.

4 Answers

In other languages, mainly JavaScript, its not needed because passing a reference to a function works, but it does not work that way with Elixir, if you refer a function with Elixir it will invoke it by default.

You can test that out by running recompile in your terminal and instantly watch a huge error appear because you tried to call a function thinking you were referencing a function.

So instead of:

def build_grid(%Identicon.Image{hex: hex} = image) do
    hex
    |> Enum.chunk(3)
    |> Enum.map(mirror_row)
  end

  def mirror_row(row) do
    # [145, 46, 200]
    [first, second | _tail] = row

    # [145, 46, 200, 46, 145]
    row ++ [second, first]
  end

you want to do this:

def build_grid(%Identicon.Image{hex: hex} = image) do
    hex
    |> Enum.chunk(3)
    |> Enum.map(&mirror_row/1)
  end

  def mirror_row(row) do
    # [145, 46, 200]
    [first, second | _tail] = row

    # [145, 46, 200, 46, 145]
    row ++ [second, first]
  end

The ampersand says I am about to pass a reference to a function. The function I am passing reference to is mirror_row and then critically at the end I have /1 which means if I have multiple versions of the function called mirror_row defined, I specifically want the one that takes one argument, an arity of one and I am doing that because mirror_row takes one argument in the example above.

It could be just because of Erlang design. In Erlang, those function argument passed without special form fun Module:Function/Arity are interpreted as atoms in Erlang VM and thus it causes an exception.

So, the reason could be to make it easier to transform Elixir code to be compatible to be run in BEAM. And the decison of Erlang require that clumsy form might want it to be more specific and help compiler easier to find bug of passing function with the wrong arity.

And for elixir, function can be applied without bracket and arguments are eagerly evaluated as well. So it's hard to determine if you want to apply get_env or pass get_env as function argument

Capture operator (&) in Elixir is same as reference operator in other languages, with extended functionality.

Reference gives an unique identifier and description of an Elixir objects: data types, structures (structure is Elixir equivalent of OOP object) and functions.

Reference differentiates between the 'content' of the object and the object itself. To be able to distinguish when function is called from when it is passed as parameter to other functions to use, reference is used.

Elixir is a functional language, which means it treats computation as the evaluation of mathematical functions and uses immutable variables.

Simplified, functions can be passed around like variables and all results are just pushed from one function to another, which should give some unexpected advantages and leads to a huge explosion of power when using.

# Function can be defined as a named element of module/structure,
# anonymous function, or function/functional variable

# define functional variable by matching it to anonymous function:
# (double is a reference of anonymous function f(x) = x + x)
double = fn x -> x + x end

# execute a function linked to a functional variable:
double.(2)      # calculate f(2) = 2 + 2
# give a reference to a functional variable:
# (reference can be used to call the function with various parameters)
&double.(&1)

# Enum.each(enum, fun/1) is a very common function used in elixir
# It will iterate through all elements of an enumerable collection
# and call the fun/1 with the element as a parameter
# Parameters are enumerable collection and reference to a function
# with one parameter

# Lets double all elements in cleanest way of writing:
Enum.each([1, 2, 3], fn x -> x + x end)
# same as previous line, written in shorter way using capture
# &1 means reference to first parameter
Enum.each(1..3, &(&1 + &1))
# same functionality, using previously defined functional variable double
Enum.each(1..3, fn x -> double.(x) end)
# same as previous line, written in shorter way using capture
Enum.each(1..3, &double.(&1))
# same functionality, but prints out the results
Enum.each(1..3, fn x -> IO.puts double.(x) end)
# same as previous line, written in shorter way using capture
Enum.each(1..3, &IO.puts double.(&1))

Personally, i prefer to avoid capture when possible. Although it shortens the expressions, when it gets complicated, it is easy to overlook the intention and misunderstand the expression.

Related