Elixir: Which Modules Are Using My Module
In Elixir, getting a list of modules that use
a specific module is trickier than you'd think. It's caused me, and others, some grief.
The naive approach is to mark the module (say, with a special function):
defmacro __using__(_opts) do
quote location: :keep do
def __my_module(), do: :ok
end
end
Then iterate through the modules with something like :code.all_loaded/0
and look for our special function. But in Elixir, modules are lazily loaded and, there's a good chance that at the time you call :code.all_loaded/0
the modules that you're looking for haven't been loaded it.
Idiomatically, I haven't found a great solution to this problem. The best seems to be to explicit pass the list of modules, either via configuration or as an argument to to a process. But this highlights a major pain point in Elixir: requiring a deep hierarchy of dependencies to be configured at the root app. Dave Thomas spoke about this problem, as well as others, at EMPEX.
At one point, we got pretty desperate to make this type of auto discovery work, and had our __using__
write the caller's name to a DETS file, which could then be read at runtime. It worked, but it was ugly.
More recently, I came up with a <airquote>better</airquote> solution.
First, we'll use :application.loaded_applications/0
to get all the applications, then :application.get_key/2
to get all the application's modules (which gives us all the names, even if they aren't loaded):
applications = :application.loaded_applications()
modules = Enum.reduce(applications, [], fn {app, _desc, _version}, acc ->
{:ok, modules} = :application.get_key(app, :modules)
# TODO
end)
Now, if we wanted to, we could just iterate through modules
and call Code.ensure_loaded
and look for our special function. This essentially circumvents Elixir's lazily loading:
Enum.reduce(modules, acc, fn module, acc ->
Code.ensure_loaded(module)
# Does this module export our special __prepared_statements/0 function?
case Keyword.get(module.__info__(:functions), :__prepared_statements) do
nil -> acc
0 -> [module | acc] # yes, it does, add it to the list
end
end)
It would be nice if we could make this more surgical and minimize the amount of modules we have to force load. This part is a little ugly, but it isn't too bad. What I do is add a special module to the app/libary.. Only if this module is present are the modules loaded and probed.
applications = :application.loaded_applications()
state = Enum.reduce(applications, [], fn {app, _desc, _version}, acc ->
{:ok, modules} = :application.get_key(app, :modules)
auto_registry? = Enum.any?(modules, fn m ->
String.starts_with?(to_string(m), "Elixir.Special.Registry.Auto.")
end)
case auto_registry? do
false -> acc
true -> scan_app(modules, acc) # same code as above
end
end)
To make this work, you just need to drop an empty module in the app/library you want scanned. I use a macro to create this:
defmacro auto_discover() do
module = Module.concat(Special.Registry.Auto, __CALLER__.module)
quote do
defmodule unquote(module) do
end
end
end
Since I've seen this question asked a number of times, I hope this post will prove helpful. I also hope that future versions of Elixir address this problem.