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.
An Elixir function is typically named, and is called a "named function". Functions are defined with the def keyword followed by a parameter list. A function also has a body containing the contents of the function. Like modules, the function body begins with the do keyword and ends with the end keyword.
The ampersand is called the capture operator in Elixir. The first & effectively either gets a reference to the original function or starts an anonymous function. Any ampersand after the first that references the positional argument passed to it. You could use this to write very concise anonymous functions.
__MODULE__ is a compilation environment macros which is the current module name as an atom.
In your example above, you have bound the anonymous function fn x -> x + x end
to the variable double
. The capture operator is used when you're passing a named function. T save/pass a named function, you need a way to indicate that it's a named function, and not a variable. This is where you use the capture &name/arity
syntax.
defmodule FunWithFuns do
def get_env, do: Application.get_all_env(:my_app)
def get_env(item), do: Application.get_env(:my_app, item)
def some_function do
IO.inspect get_env
Enum.map([:item1, :item2], get_env)
end
end
How do you resolve the get_env
in this case? Is it a call to get_env/0
, or is it a reference to get_env/1
? In the case of anonymous functions, double
is the variable binding and double.(1)
is the invocation of the function bound to the variable double
.
Note that calling zero-arity functions without the ()
has been deprecated but still works. I suppose that once that is removed perhaps the compiler could make the choice, but even then there may be other reasons why it would not work.
Another reason: let's assume, for example, that we did support using the named function name. How could we support this:
# contrived example
defmodule MoreFunWithFuns do
def fun1, do: :something_stateful
def fun1(x), do: x + 1
def higher(list, fun) do
cond do
is_function(fun, 0) -> fun.() |> process_state
is_function(fun, 1) -> Enum.map(list, fun) |> process_state
end
end
def run(list) do
higher(list, fun1) # which fun1 here?
end
end
A variable can only have one binding at time. So, there is no ambiguity as to what it's referencing. However, a named function can have multiple clauses with different arities. So, if we provide just the function name, there is ambiguity as to which clause we are referring.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With