lkuty

lkuty

Define a function at compile-time

I have a module for which I want to create a function rule_ids/0 at compile time. The code collects the rule ids at compile time from the first argument of all clauses of the function condition/3 using the AST.
It does not work because the variable rule_ids_ defined at compile time is not accessible when defining the function with def rule_ids(), do: rule_ids_.
How could I proceed to get it working? Should I use a macro somewhere?

defmodule Rules do

  # Collect the rule ids at compile time
  {_ast, rule_ids_} = File.read!(__ENV__.file)
    |> Code.string_to_quoted()
    |> Macro.traverse([],
      fn
        {:def, _, [{:condition, _, [rule_id, {:v, _, nil}, {:site, _, nil}]} | _]} = x, acc -> {x, [rule_id | acc]}
        other, acc -> {other, acc}
      end,
      fn other, acc -> {other, acc} end
    )

  # How could I use rule_ids_ here?
  def rule_ids(), do: rule_ids_

  def condition(rule_id, v, site)

  def condition(1018, v, site) do
    v[site][7018] < 50 && v[site][7019] < 50 && v[site][7020] < 50
  end

  def condition(1017, v, site) do
    v[site][7027] > 42
  end

  # ...

end

Marked As Solved

LostKobrakai

LostKobrakai

There are better ways to do this over reading “yourself” and parsing the AST.

You can use @on_definition on a module (say A) to have it call a macro or function elsewhere (Say on B) whenever a function or macro is defined on the module. With a macro being provided you can then pull information from those definitions and store them in an arbitrary module attribute of A.

You can register another callback @before_compile in A, which calls the callback macro (likely also in B) right before the module starts compiling, but after all it’s body has been evaluated. That module can then read your arbitrary module attribute for all the gathered information and turn it into AST returned from the macro. That AST is then added to the module – you can imagine it being injected right before the end line of defmodule.

Also Liked

mudasobwa

mudasobwa

Creator of Cure

While everything suggested above is absolutely correct, I’m to answer your original question.

One does not technically need module attributes, nor hooks here. The issue is scopes and we have unquote fragments for that.

rule_ids_ is defines in the outer scope for rule_ids/0 call, therefore one needs to unquote it there. THe code below would work (I also fixed the issue with condition/3 head mistakenly falling under a match during the parse stage.)

defmodule Rules do

  # Collect the rule ids at compile time
  {_ast, rule_ids_} = File.read!(__ENV__.file)
    |> Code.string_to_quoted()
    |> Macro.traverse([],
      fn
        {:def, _, [{:condition, _, [rule_id, {:v, _, nil}, {:site, _, nil}]} | _]} = x, acc 
            when not is_tuple(rule_id) ->
          {x, [rule_id | acc]}

        other, acc ->
         {other, acc}
      end,
      fn other, acc -> {other, acc} end
    )
  # print it out and see how it goes, this line is to be removed
  |> tap(& &1 |> Macro.to_string() |> IO.puts())

  # unquoting from the outer scope
  # HERE              ⇓⇓⇓⇓⇓⇓⇓                 
  def rule_ids(), do: unquote(rule_ids_)

  def condition(rule_id, v, site)

  def condition(1018, v, site) do
    v[site][7018] < 50 && v[site][7019] < 50 && v[site][7020] < 50
  end

  def condition(1017, v, site) do
    v[site][7027] > 42
  end
  # ...
end
LostKobrakai

LostKobrakai

Yeah, that’s what I was hinting at. You can remove the hardcoded Rules in the Hooks module by replacing it with env.module. Often all the boilerplate you have in Rules is also contained in a __using__ macro of Hooks, so a use Hooks does everything you need within Rules.

D4no0

D4no0

These is what module attributes are for: Module — Elixir v1.20.2

I’ve never checked it, but this should work:

  Module.register_attribute(__MODULE__, :rule_ids)

 # Collect the rule ids at compile time
  {_ast, rule_ids_} = File.read!(__ENV__.file)
    |> Code.string_to_quoted()
    |> Macro.traverse([],
      fn
        {:def, _, [{:condition, _, [rule_id, {:v, _, nil}, {:site, _, nil}]} | _]} = x, acc -> {x, [rule_id | acc]}
        other, acc -> {other, acc}
      end,
      fn other, acc -> {other, acc} end
    )
  
  Module.put_attribute(__MODULE__, :rule_ids, rule_ids_)

  # How could I use rule_ids_ here?
  def rule_ids(), do: @rule_ids

  def condition(rule_id, v, site)

  def condition(1018, v, site) do
    v[site][7018] < 50 && v[site][7019] < 50 && v[site][7020] < 50
  end

  def condition(1017, v, site) do
    v[site][7027] > 42
  end

  # ...

end

Last Post!

mudasobwa

mudasobwa

Creator of Cure

While everything suggested above is absolutely correct, I’m to answer your original question.

One does not technically need module attributes, nor hooks here. The issue is scopes and we have unquote fragments for that.

rule_ids_ is defines in the outer scope for rule_ids/0 call, therefore one needs to unquote it there. THe code below would work (I also fixed the issue with condition/3 head mistakenly falling under a match during the parse stage.)

defmodule Rules do

  # Collect the rule ids at compile time
  {_ast, rule_ids_} = File.read!(__ENV__.file)
    |> Code.string_to_quoted()
    |> Macro.traverse([],
      fn
        {:def, _, [{:condition, _, [rule_id, {:v, _, nil}, {:site, _, nil}]} | _]} = x, acc 
            when not is_tuple(rule_id) ->
          {x, [rule_id | acc]}

        other, acc ->
         {other, acc}
      end,
      fn other, acc -> {other, acc} end
    )
  # print it out and see how it goes, this line is to be removed
  |> tap(& &1 |> Macro.to_string() |> IO.puts())

  # unquoting from the outer scope
  # HERE              ⇓⇓⇓⇓⇓⇓⇓                 
  def rule_ids(), do: unquote(rule_ids_)

  def condition(rule_id, v, site)

  def condition(1018, v, site) do
    v[site][7018] < 50 && v[site][7019] < 50 && v[site][7020] < 50
  end

  def condition(1017, v, site) do
    v[site][7027] > 42
  end
  # ...
end

Where Next?

Popular in Questions Top

RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
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
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
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
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

Other popular topics Top

electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
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
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 44608 311
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New

We're in Beta

About us Mission Statement