Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

elixir

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.

like image 517
Thomas Browne Avatar asked May 30 '17 15:05

Thomas Browne


People also ask

How do you define a function in Elixir?

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.

What is the ampersand in Elixir?

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.

What is __ module __ In Elixir?

__MODULE__ is a compilation environment macros which is the current module name as an atom.


4 Answers

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.

like image 72
Steve Pallen Avatar answered Oct 16 '22 19:10

Steve Pallen


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.

like image 40
Daniel Avatar answered Oct 16 '22 20:10

Daniel


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

like image 20
bruteforcecat Avatar answered Oct 16 '22 20:10

bruteforcecat


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.

like image 45
graziani Avatar answered Oct 16 '22 19:10

graziani