lkuty

lkuty

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

Showing Posts 1 to 8

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

lkuty

lkuty OP

It worked with:

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

lkuty OP

I’ve put @on_definition {Hooks, :rule_ids} in module Rules but in the other module called Hooks I cannot see how I can accumulate the ids and make use of them in the rule_ids/6 function.

defmodule Hooks do
  Module.register_attribute(__MODULE__, :rule_ids, accumulate: true, persist: false)

  def rule_ids(_env, :def, :condition, [rule_id | _], _guards, _body)
  when is_integer(rule_id)
  do
    IO.puts rule_id
  end
  def rule_ids(_env, _kind, _name, _args, _guards, _body), do: nil
end
LostKobrakai

LostKobrakai

You can use Module.put_attribute, though I think Hooks.rule_ids needs to be a macro for that to work. The module attribute needs to be one on the module being compiled though, not on the Hooks module.

lkuty

lkuty OP

I managed to make it work using both your advices. In fact I thought you had given two different ways of doing things but it looks like we have to combine them.

defmodule Rules do
  Module.register_attribute(__MODULE__, :rule_ids, accumulate: true, persist: false)

  @on_definition {Hooks, :rule_ids}
  @before_compile {Hooks, :create_rule_ids}

# ...
end
defmodule Hooks do
  def rule_ids(_env, :def, :condition, [rule_id | _], _guards, _body)
  when is_integer(rule_id)
  do
    Module.put_attribute(Rules, :rule_ids, rule_id)
  end
  def rule_ids(_env, _kind, _name, _args, _guards, _body), do: nil

  defmacro create_rule_ids(_env) do
    quote do
      def rule_ids, do: unquote(Module.get_attribute(Rules, :rule_ids))
    end
  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.

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
— All posts loaded —

Where Next? Top

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
water
I’m aware of 2 such projects, are there others? git: GitHub - team-alembic/realworld: A fullstack Phoenix LiveView application with back...
New
katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
peck
Hi everyone. My team and I have been working on a fairly modest app based around video streaming and chat, but we’ve landed a customer t...
New
bradley
I really like the adapter patterns that ecto, nebulex, waffle, etc. use and would love find something similar for a key management servic...
New
unaware8150
Hello folks! So at work, we are seeing some situations where we have to define some “fixed” strings that are used across the codebase in...
New
Alvinkariuki
How Can I Optimise Compile Time Dependencies I have been building an elixir application for about 2 years now. Many modules and files ha...
New

Other Trending Topics Top

KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
budgie
I love Elixir. It’s one of 2 programming languages I’ve ever fallen in love with. But I don’t use it anymore. Serverless was the promis...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews