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.
First Post!
03juan
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.20.2 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
Last Post!
kartheek
I am replying to your questions in this post:
Point - 1) Anonymous functions, captured functions, etc - covered in previous post. & operator creates anonymous functions in cases 3, 4 like expanding short form anonymous functions defined using &() or &{} or & and partially applying functions - As you are modifying second parameter of the Test.test function using Map.get - it will create an anonymous function (see example capture_5) .
Point - 3) True
validates() macro is expanded before __before_compile__ , but it appears lexically in the bottom of the file (till here - was in that sense). Anonymous function assignment to a module attribute directly or indirectly does not cause error. Module attributes are available only during compile time - any usage of module attribute with anonymous function in a func/macro will cause compiler error - in this case __before_compile__ is causing error.
Have a nice weekend.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #security
- #hex









