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








