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

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
lkuty

lkuty

It worked with:

Module.register_attribute(__MODULE__, :rule_ids, accumulate: false, persist: false)

Where Next?

Popular in Questions Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
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
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
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
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
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
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
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
kostonstyle
Hi all I want to have a unix time, from the current time plus 1 hour. DateTime.now + 1 hour How to get it in elixir? Thanks
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

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
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
sergio
I couldn’t find any guides that worked well with Phoenix 1.6.0 and esbuild. I hope this helps people test the waters and eases you into t...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
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
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
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
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
beno
I will often find my self writing things similar to: case some_value do nil -&gt; something() "" -&gt; something() _ -&gt; someth...
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

We're in Beta

About us Mission Statement