DEV Community

Mete Can Eris
Mete Can Eris

Posted on

Listing Modules under a Namespace in Elixir

In Elixir, it is possible to get a list of compiled modules under an application using Erlang's application module.

Let's first see how we can obtain a list of all compiled modules, then we'll discuss the why and what.

How to get a list of compiled modules

I'll try to dig in and reference relevant tools by building upon this Stack Overflow answer by Aleksei Matiushkin.

Reading from Erlang's Reference Manual, the application module interacts with application controller, a process started at every Erlang runtime system. This module contains functions for controlling applications (for example, starting and stopping applications), and functions to access information about applications (for example, configuration parameters).

In our code, we can communicate with the application module via the lowercase atom :application, just like the rest of Erlang’s extensive standard library. All Erlang modules are represented by lowercase atoms such as :os and :timer.

:application.get_key/2 will return a list of all compiled modules if given :modules atom as the second argument.

{:ok, modules} = :application.get_key(:app_name, :modules)
[
 App,
 App.ModuleA, App.ModuleA.ModuleX,
 App.ModuleB, App.ModuleB.ModuleY
]
Enter fullscreen mode Exit fullscreen mode

We can then:

  • filter certain modules by binary pattern matching or by using split/1 and concat/1 functions under Module
  • call their functions (using Kernel's apply/3)
  • do whatever that serves our purpose by using the capabilities of the Stream and Enum modules.
{:ok, modules} = :application.get_key(:app_name, :modules)

modules
|> Stream.map(&Module.split/1)      # split
|> Stream.filter(fn module ->       # filter
  case module do
    ["App", "Namespace", "Base"] -> false
    ["App", "Namespace", _] -> true
     _ -> false
  end
end)
|> Stream.map(&Module.concat/1)     # concat
|> Stream.map(&{&1, apply(&1, :some_module_fn, [])})
|> Enum.map(fn output ->
  # ... do whatever
end)
Enter fullscreen mode Exit fullscreen mode

Why do this?

why would you do that

Well, besides the fact that we can, I'll give a few examples from my own projects.


In Rubik (a work-in-progress visual scripting language implementation for a thesis project), I represented the visual computation nodes as Elixir modules conforming to a specific Protocol, such as Nodes.Arithmetic.Add, Nodes.Logic.And, or Nodes.List.Count.

Rubik is an open-source library, so developers integrating the library into their applications can code new nodes (specific to their use cases) that their users then can use in the visual scripting interface.

The visual scripting interface provides a dropdown menu that a user can select a node from. This dropdown is essentially populated by a list of available modules under the Nodes namespace via a similar pipeline.

So to add a new node, all a developer has to do is to code a module under the Nodes namespace and compile, and the node appears in the dropdown.


In Watchtower, I try to categorize smart contracts based on their interface via available ABIs (Application Binary Interface). Again, I represented smart contract types as Elixir modules living under a Contracts namespace.

Each module has a module attribute @schema that contains a subset interface that I use to match an ABI to a contract type.

  @schema [
    {"name", [], [:address]},
    {"symbol", [], [:string]},
    {"token0", [], [:address]},
    {"token1", [], [:address]},
  ]
Enter fullscreen mode Exit fullscreen mode

Each module also implements (better yet adopts via a Behaviour) a match/1 function that compares the given ABI to the subset under @schema. Basically, the pipeline becomes a meta factory that returns the correct module given an ABI.

Of course, it's always possible to implement a factory module that would do the same thing, but this way, a single module attribute gets the whole job done.

Top comments (0)