Hello,
I want to create a unique identifier for a module that will be passed to using as a string, example:
use Identifier, :unique_identifier
Now, in Identifier module I do the following:
defmacro __using__(name) when is_atom(name) do
quote do
def __reporter_unique_id__(), do: unquote(name)
end
end
What I want to do before this is to ensure that there are no duplicated names like this one, if yes I want to throw a compile-time error. I was thinking about using a GenServer or Agent, however I cannot understand when should I start it. I’ve tried to start it inside of Identifier module body, however it doesn’t seem to work.
I use a related approach in ex_cldr to cache locale data at compile time. It starts a GenServer hosting an ets table on demand. The relevant code is here in case any of it is useful to you.
I’d suggest you go with something more formal and polished as @kip’s.
In the meantime, here’s a hacky solution that gives you a compile-time error:
defmodule IdRegistryState do
use Agent
def start_link(_initial_value), do: Agent.start_link(fn -> %{} end, name: __MODULE__)
def register(name) do
Agent.get_and_update(__MODULE__, fn table ->
case Map.fetch(table, name) do
{:ok, _value} -> {{:id_already_registered, name}, table}
:error -> {:ok, Map.put(table, name, 1)}
end
end)
end
end
defmodule IdRegistry do
defmacro __using__(name) when is_atom(name) do
IdRegistryState.start_link(:ignored)
quote bind_quoted: [name: name] do
:ok = IdRegistryState.register(name)
def __reporter_unique_id__(), do: unquote(name)
end
end
end
defmodule Z1 do
use IdRegistry, :z1
end
defmodule Z2 do
use IdRegistry, :z2
end
defmodule Z3 do
use IdRegistry, :z1
end
You will get a compile-time error and stacktrace similar to this:
== Compilation error in file lib/play.ex ==
** (MatchError) no match of right hand side value: {:id_already_registered, :z1}
lib/zyx.ex:37: (module)
(stdlib 3.17) lists.erl:1267: :lists.foldl/3
That should be a good starting point for you.
Couple of notes:
Calling IdRegistryState.start_link(:ignored) every time you do use IdRegistry, :name is hacky but I was unable to make this Agent start in the app’s supervision tree before it’s needed so I just removed it from there and made the start_link call not assert on return value (and it will return {:already_started, pid} on the 2nd use and on; however that will not impact the already-running unique Agent in any way)
No real reason for separating both modules, try merging them into one if you like.
Calling IdRegistryState.start_link(:ignored) every time you do use IdRegistry, :name is hacky but I was unable to make this Agent start in the app’s supervision tree before it’s needed so I just removed it from there and made the start_link call not assert on return value (and it will return {:already_started, pid} on the 2nd use and on; however that will not impact the already-running unique Agent in any way)
Looks like @kip is using the same approach, all his api functions start the genserver if it is not started. It seems it is not possible to start the agent before compilation process.
Everything works ideally while you use the comandline. The problem appears when using elixir-ls, the compilation server never stops, so the genserver never gets stopped, creating duplicates and error messages inside of code editor.
Now that I think about this more, this method seems an anti-pattern, it will not work reliably if there are cached compiled files, since it won’t trigger compilation for them, the possible solution would be to look at (compiled modules?)
Looks like my problem is a duplicate of this and there is no reliable solution to do this.
Isn’t what you are describing only an issue on development machines? CI and production deployments always build from scratch so you won’t have issues with intermittent build artifacts.
I don’t think it’s necessary for what you’re trying to do, but since you referenced the approach where you add a compilation step that reads module attributes (like with protocol consolidation), I figure I’ll share an experiment I did trying to understand it more.
The idea was a library to generalize that solution, and while I wouldn’t use it in production, it works, and it could be helpful to reference.
Actually this facility is for development mostly. We have modules that contain business-logic and are used by the application, currently we keep track of them by their module name inside of database, it works, however I wanted to decouple elixir-specific functionality, to facilitate easier migration to other projects.
The idea is to make it as dumb-proof as possible, since on these projects work mostly inexperienced people. Having the macro check for duplicates and throw compile-time error with good message makes them implement the functionality correctly.
Now that I think more about this, a approach similar to how phoenix routes work is well suited here and can solve the problem, at the cost of maintaining the mappings list.