Vex validate w anon. function: cannot inject attribute @vex_validations into function/macro because cannot escape #Function

@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) :

  1. & captures a function - &Module.function/arity - same as Function.capture(Module, function, arity) - &Test.test/2 is same as Function.capture(Test, :test, 2)

  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

  3. & 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.

  4. & 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)

1 Like