Listing Modules under a Namespace in Elixir
Mete Can Eris
Posted on February 21, 2022
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
]
We can then:
- filter certain modules by binary pattern matching or by using
split/1
andconcat/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)
Why do this?
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]},
]
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.
Posted on February 21, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.