chemist

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

03juan

Providing a stacktrace of your error would help in debugging.

Most Liked

kartheek

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

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

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

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

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.

Where Next?

Popular in Questions Top

vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 131117 1222
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 55125 245
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New

We're in Beta

About us Mission Statement