@chemist You are welcome.
Kernel.SpecialForms.&/1 or capture operator - terminology of capturing or creating anonymous function is little confusing in the docs (speaking for myself) :
-
& captures a function - &Module.function/arity
- same as Function.capture(Module, function, arity)
- &Test.test/2
is same as Function.capture(Test, :test, 2)
-
Another way of capturing function is &Module.function(&1, &2,.. &n)
- this should match the number of parameters for a given function. No modification of parameters. &Test.test(&1, &2)
captures function and is same as &Test.test/2
-
& can create an anonymous function - &()
which is short form for fn -> end
. &(&1 + 2)
is expanded to fn x -> x + 2 end
. {} and can be used for tuples and lists.
-
& can partially apply a function - &Test.test(&1, &2.funds_balance)
(note - here we don’t use arity /n). function call looks similar to 1 and 2 - as we are modifying second param - it creates an anonymous function - fn x, x1 -> Test.test(x, x1.funds_balance) end
Lets look at the following example:
defmodule TestCapture do
def capture_1() do
x = &Test.test/2
x.(1, 2)
end
def capture_2() do
x = &Test.test(&1, &2)
# rewritten as
# x = &Test.test/2
x.(1, 2)
end
def capture_3() do
x = Function.capture(Test, :test, 2)
# expanded as
# x = :erlang.make_fun(Test, :test, 2)
x.(1, 2)
end
def capture_4() do
x = &Test.test(&1, &2.funds_balance)
# expanded as
# x = fn x1, x2 -> Test.test(x1, x2.funds_balance) end
x.(1, 2)
end
def capture_5() do
x = &Test.test(&1, Map.get(&2, :funds_balance))
# expanded as
# x = fn x1, x2 -> Test.test(x1, Map.get(x2, :funds_balance)) end
x.(1, 2)
end
def capture_6() do
x = &(&1 + &2 + 1)
# expanded as
# x = fn x1, x2 -> :erlang.+(:erlang.+(x1, x2), 1) end
x.(1, 2)
end
end
You can see how - Elixir compiler expands the module using BeamFile.elixir_code!(TestCapture) |> IO.puts()
and ast using BeamFile.debug_info(TestCapture)
.
All the three function captures - capture_1, capture_2, capture_3 generate same byte code - calling function directly (no anonymous function).
{:function, :capture_1, 0, 9,
[
{:line, 1},
{:label, 8},
{:func_info, {:atom, TestCapture}, {:atom, :capture_1}, 0},
{:label, 9},
{:move, {:integer, 2}, {:x, 1}},
{:move, {:integer, 1}, {:x, 0}},
{:line, 2},
{:call_ext_only, 2, {:extfunc, Test, :test, 2}} # <- this one
]},
{:function, :capture_2, 0, 11,
[
{:line, 3},
{:label, 10},
{:func_info, {:atom, TestCapture}, {:atom, :capture_2}, 0},
{:label, 11},
{:move, {:integer, 2}, {:x, 1}},
{:move, {:integer, 1}, {:x, 0}},
{:line, 4},
{:call_ext_only, 2, {:extfunc, Test, :test, 2}} # <- this one
]},
{:function, :capture_3, 0, 13,
[
{:line, 5},
{:label, 12},
{:func_info, {:atom, TestCapture}, {:atom, :capture_3}, 0},
{:label, 13},
{:move, {:integer, 2}, {:x, 1}},
{:move, {:integer, 1}, {:x, 0}},
{:line, 6},
{:call_ext_only, 2, {:extfunc, Test, :test, 2}} # <- this one
]},
Kernel.SpecialForms — Elixir v1.16.0 documentation gives an example of Kernel.is_atom and states below:
Capture operator. Captures or creates an anonymous function.
fun = &Kernel.is_atom/1
fun.("string")
In the example above, we captured Kernel.is_atom/1 as an anonymous function and then invoked it.
It should be read as below:
Capture operator. Captures a function or creates an anonymous function.
In the example above, we captured Kernel.is_atom/1 and it can be invoked using the same syntax as anonymous function.
Essence of capturing a function is storing reference to a function in a variable to be passed around and invoked using the variable. This has nothing to do with anonymous functions or closures except that it uses func_name.
syntax for invoking the function.
May be separating concepts like function variables, anonymous functions, captured functions and invoking a function in function variable will remove this confusion.
For those who are curious - all the above code from beam file is inspected using BeamFile - BeamFile.byte_code(TestCapture
), BeamFile.elixir_code!(TestCapture) |> IO.puts()
and BeamFile.debug_info(TestCapture)