chemist
Vex validate w anon. function: cannot inject attribute @vex_validations into function/macro because cannot escape #Function
Hi, Im having a bit of trouble with anon. functions using the Vex validator.
defmodule Test do
@enforce_keys [:wallet_id, :withdrawal, :currency, :funds_balance]
defstruct [
:wallet_id,
:withdrawal,
:currency,
:funds_balance
]
use ExConstructor
use Vex.Struct
alias __MODULE__
validates(:withdrawal, presence: [message: "expected"],
by: [function: &Test.test(&1, Map.get(&2, :funds_balance)),
message: "invalid withdrawal."])
def test(withdrawal, funds_balance) do
# some logic here
end
end
The above has two capture operators, &1 which is the field being validated: withdrawal and &2 which is the entire Test struct, %Test{}
Basically the above snippet results in this error:
** (ArgumentError) cannot inject attribute @vex_validations into function/macro because cannot escape #Function
I need a nested function that can effectively capture the second field, &2. I am aware that it is possible to move this function to the function, test but will like to learn if it is possible to do it as a nested function as part of an anonymous function
Many thanks.
Most Liked
kartheek
@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 asFunction.capture(Module, function, arity)-&Test.test/2is same asFunction.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 forfn -> end.&(&1 + 2)is expanded tofn 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)
03juan
Officially you can only have a function with arity 1. There seems to be some support within the code for arity 2 but the Vex.Struct implementation does not use it.
If you follow the Vex.Struct and Vex.Extract.Struct code you may be able to re-implement them to provide your own validates/3 that takes a context, but that would be a more advance solution at the moment.
I respectfully disagree. The Kernel.SpecialForms.&/1 docs say
Capture operator. Captures or creates an anonymous function.
So in this case OP is trying to create an anonymous 2-arity function that calls test/2, but the parsing of @validators is failing because it does seem to expect the form &function/arity
chemist
Hi kartheek, yes that is what I did as explained in my original post, I still want to find out whether a nested function can be done.
I am aware that it is possible to move this function to the function, test but will like to learn if it is possible to do it as a nested function as part of an anonymous function
kartheek
I have not created an anonymous function, I captured the function using & operator. Look at the following:
iex(6)> a = &Test.test/2
&Test.test/2
iex(7)> i a
Term
&Test.test/2 #<- it is pointing to Test.test func
Data type
Function
Type
external
Arity
2
Implemented protocols
Enumerable, IEx.Info, Inspect, Jason.Encoder, Phoenix.Param, Plug.Exception, Swoosh.Email.Recipient
iex(8)> a = &Test.test(&1, &2.funds_balance)
#Function<43.79398840/2 in :erl_eval.expr/5>
iex(9)> i a
Term
#Function<43.79398840/2 in :erl_eval.expr/5>
Data type
Function
Type
local
Arity
2
Description
This is an anonymous function. # <- anonymous function
Implemented protocols
Enumerable, IEx.Info, Inspect, Jason.Encoder, Phoenix.Param, Plug.Exception, Swoosh.Email.Recipient
You might have read it - you may have missed few things.
Github issue says you cannot use anonymous functions - as they are closures and their environment does not survive once compilation is finished - in this case of defining as module attribute(@vex_validations) and using them in a function(vex_validations). Closures and anonymous functions are little difficult to explain here - you can read more about them on internet.
Read my comments inline:
defmodule Vex.Struct do
@moduledoc false
defmacro __using__(_) do
quote do
@vex_validations %{}
@before_compile unquote(__MODULE__)
import unquote(__MODULE__)
def valid?(self), do: Vex.valid?(self)
end
end
"""
below macro tries to inject a __vex_validations__ function which refers to @vex_validations,
which has anonymous function. anonymous function cannot be injected by elixir compiler.
"""
defmacro __before_compile__(_) do
quote do
def __vex_validations__(), do: @vex_validations
require Vex.Extract.Struct
Vex.Extract.Struct.for_struct()
end
end
"""
below macro creates a module attribute @vex_validations on the module.
This works fine - no problems till here.
"""
defmacro validates(name, validations \\ []) do
quote do
@vex_validations Map.put(@vex_validations, unquote(name), unquote(validations))
end
end
end
You can try to compile the following code(it will fail), this is similar to what Vex.Struct macros are doing:
defmodule Test do
@test_map %{function: fn -> 1 end}
def test_map, do: @test_map
end
TLDR: You are creating an anonymous function and it does not work in case of Vex.Struct due compiler limitations.
chemist
Many thanks kartheek for your kind and comprehensive reply. I took some time to ponder and try out the samples you helpfully supplied.
You make sense. Can kindly see my responses to check my understanding
&Test.testis NOT an anonymous function. It is NAMED Test.test whereTestis the module,testthe function.&is a capture operator and ONLY when applied to an unnamed function e.g.fn x -> x + 1 end, that function is treated as an anonymous function USING a capture operator e.g.&(&1 + 1).()
A question though: My earlier post above had a nested &Map.get(&2, :funds_balance) - is this an anonymous function? Or a named one?
- My mistake
You might have read it - you may have missed few things.
- So in essence, there is NO way to use the
validatesmacro exposed byVex.structfor an anonymous function so my method of defining a seperate function is correct (which you also suggested)
But just a quick question: I dont understand one of your inline comments:
What does “no problems till here” mean? Do you mean this function should execute without problems?
Many thanks and have a great weekend.







