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.

Providing a stacktrace of your error would help in debugging.

Many thanks. Not too sure if it helps but here goes.

== Compilation error in file test.ex ==
** (ArgumentError) cannot inject attribute @vex_validations into function/macro because
 cannot escape #Function<0.45330970/2 in :elixir_compiler_5.__MODULE__/1>. The 
 supported values are: lists, tuples, maps, atoms, numbers, bitstrings, PIDs and remote 
 functions in the format &Mod.fun/arity
    (elixir 1.13.2) lib/kernel.ex:3509: Kernel.do_at/5
    (elixir 1.13.2) expanding macro: Kernel.@/1
    (test 0.1.0) test.ex:1: Test.__vex_validations__/0
    (vex 0.9.0) d:/test.ex:1: Vex.Struct.__before_compile__/1

function is supposed to be capture like &Test.test/1 or &Test.test/2.

Hi kartheek, apologies but if i understood you correctly, you are saying change the statement to:

  validates(:withdrawal, presence: [message: "expected"], 
  by: [function: &Test.test/2(&1, Map.get(&2, :funds_balance)), 
  message: "invalid withdrawal."])

Adding the arity operator to the function &Test.test does not help.

Its supposed to be

 by: [function: &Test.test/2,

https://hexdocs.pm/elixir/1.12.3/Function.html#module-the-capture-operator

It accepts function of arity 1 or 2 - if it is 2 it is passing context as second param.

You might have to relook at your test function

Not always true as the examples show passing an anonymous function definition

https://hexdocs.pm/vex/Vex.Validators.By.html?#module-examples

Vex.Validators.By.validate(
  [], [function: fn (v) when is_list(v) -> :ok
                    (v) -> {:error, {:not_list, v}} end])
:ok

The docs say

Options

None, a function with arity 1 must be provided.

  • :function: The function to check. Should have an arity of 1 and return true/false.

But the code you posted does seem to accept arity 2.

I assumed validates can be called with 3 arguments, but the code only provides VexStruct.validates/2

OP is not using anonymous function - wants to invoke Test.test ?

  validates(:withdrawal, presence: [message: "expected"], 
  by: [function: &Test.test(&1, Map.get(&2, :funds_balance)), 
  message: "invalid withdrawal."])

Map.get(&2, :funds_balance) will evaluate to the value held in funds_balance and @enforce_keys guarantee a non nil value. And i have tested it with a non-nil value.

The function Test.test has an arity of 2 and accepts two values &1: withdrawal (which is being evaluated) and the outcome of evaluating Map.get&2: funds balance resulting in an evaluation which is either false or true

So as a result, the function for the validator above will return true or false.

What I believe is the anon function utilizing the Vex.validator does not seem to like having a nested function using a capture operator. So im checking my understanding.

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

1 Like

Check this issue - Compilation error with Vex.Struct validates by · Issue #11 · CargoSense/vex · GitHub

Yep i read this but dont understand how to use this to solve my current problem. You have any suggestions?

Vexing.

Yep but why would I need a validates/3 when I’m calling only a validates/2 ultimately after the evaluation of the inner Map.get.

By right my function call should work as it has an arity of 2. Thoughts?

Can you try this:

validates(:withdrawal, presence: [message: "expected"], 
  by: [function: &Test.test/2, 
  message: "invalid withdrawal."])

  def test(withdrawal, context) do
    # some logic here
    funds_balance = context.funds_balance
  end

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

1 Like

This says you cannot use anonymous functions when validating structs:

This is because anonymous functions are closures, which is to say that they capture the environment in which they’re created. In the case of module attributes, this is the compile time environment that is not available at runtime.

Long story short, when validating structs you have to use named remote functions IE &OtherModule.function/1

I guess you have no other option ?

@benwilson512 can elaborate more on this.

I have read the article already. But i dont fully understand how it applies to my question

This is because anonymous functions are closures, which is to say that they capture the environment in which they’re created. In the case of module attributes, this is the compile time environment that is not available at runtime.

Long story short, when validating structs you have to use named remote functions IE &OtherModule.function/1

As you have quoted this article again despite me saying I have read it, maybe you can explain to me how this case applies here.

Do note, you have successfully created an anonymous function in the validator contrary to what the article says and furthermore, this article does not comment on my question - nesting a named function within an anonymous function.

Thanks 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.

1 Like

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

  1. &Test.test is NOT an anonymous function. It is NAMED Test.test where Test is the module, test the 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?

  1. My mistake

You might have read it - you may have missed few things.

  1. So in essence, there is NO way to use the validates macro exposed by Vex.struct for 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.

1 Like

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

2 Likes