Generate a list of functions at compile-time into a module of the library

For a library, we get the list of all Ecto Schema modules of the user’s app, and want to generate functions (through macros) into a module of our library.

I can retrieve the list of Ecto schema already:

{:ok, modules} = :application.get_key(APP_NAME, :modules)

modules
|> Enum.filter(&({:__schema__, 1} in &1.__info__(:functions)))
|> IO.inspect()

Can I call :application.get_key(APP_NAME, :modules) at compile-time?

If so, I still encounter two problems:

  1. there is no concept of a “main” application in the Elixir/Erlang world, so how should I get the application(s) of the user (see APP_NAME above)? Or maybe I have to rethink and do it in another way, but kind of lost (maybe solution in point 2 below).

  2. how do I run these macros at compile-time? Ideally the user doesn’t need to write any code. Or maybe the library’s user can write a line of code at startup with the list of his applications (which would solve point 1.)? LibraryName.init([:the_app]), but it should happen at compile-time for the macros…

1 Like

As you point out, introspecting a project to “find all the modules that have some behaviour” is a challenge because a project can have many applications and there isn’t a digraph except through analysing the dependency tree.

Secondly, having been down a similar path, this kind of “magically find all the modules” tends to end badly. Part of it is the desire in Elixir to be explicit which helps remove magic. Part of it is that it ties too much to internal implementation which can and does change.

That said, there are some approaches you might consider:

  1. Mix lets you specify a compiler chain. Since you want to generate a module based upon previously compiled modules, you could create your own “compiler” which is just a function that gets called after everything else is compiled. See https://hexdocs.pm/mix/Mix.Tasks.Compile.html

  2. You can traverse dependencies by having fun with Mix.Dep.cached/0 and then doing :application.get_key(APP_NAME, :modules) or similar. You would do this in your “compiler” which would be configured to be the last compiler to run - thereby guaranteeing all the dips and app code are compiled first and therefore can be introspected.

All in all though, having travelled this road a few times, I would recommend make your intention explicit not implicit.

3 Likes