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.

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

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

kartheek

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

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

  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.

Where Next?

Popular in Questions Top

dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
pgiesin
This should be a simple problem but I just can’t seem to figure it out. I have a standalone Elixir app that won’t find the database. Dep...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
gonzofish
I’m currently trying to understand how to join three tables using Ecto. All the examples I’ve seen use 2, so maybe I’m just missing somet...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
beno
I will often find my self writing things similar to: case some_value do nil -&gt; something() "" -&gt; something() _ -&gt; someth...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New

Other popular topics Top

chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30048 115
New
shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

We're in Beta

About us Mission Statement