Elixir A Module By Any Other Name: Aliases in Elixir

Modules provide a way for us to organize our code and compose complex systems.

"A module is a collection of functions, something like a namespace. Every Elixir function must be defined inside a module. To call a function of a module, you use the syntax ModuleName.function_name(args) ."

-- Elixir in Action, pg. 22, Saša Jurić

Any module, whether part of the standard library or defined by us, is available if it has been loaded into memory or is on the BEAM's code path. It is easy to see this in practice within an IEx session. For instance, the String and Enum modules are readily accessible.

> " 1 2 3" |> String . split () |> Enum . map ( fn ( x ) -> String . to_integer ( x ) * 10 end ) [ 10 , 20 , 30 ]

Enum and String are aliases. They are a convenient syntax for referring to the various Elixir modules our code needs.

If they are aliases, then for what are they an alias?

> to_string ( Enum ) " Elixir.Enum"

When we refer to the Enum module, it turns out this is an alias for :"Elixir.Enum" . We can see that is true with a comparison.

> Enum == :"Elixir.Enum" true

I'd personally prefer to only type Enum each time I want to use the Enum module, but we are free to ignore these aliases.

> " 1 2 3" |> :"Elixir.String" . split () |> :"Elixir.Enum" . map ( fn ( x ) -> :"Elixir.String" . to_integer ( x ) * 10 end ) [ 10 , 20 , 30 ]

I cannot think of a good reason to ignore these aliases. Instead, we should take advantage of aliases, even creating some of our own using alias/2 . We can keep our code concise and readable by defining aliases for lengthy module names.

> alias MyApp . User . Account MyApp . User . Account > MyApp . Repo . get! ( Account , 1 ) Account % Account { ... }

We can even make short module names even shorter.

> alias String , as: Str String > Str . split ( " 1 2 3" ) [ " 1" , " 2" , " 3" ]

How does our code know that Str and String and :"Elixir.String" all refer to the same thing? The compiler makes it so. Elixir creates the String alias and we created the Str alias. When the compiler is processing our source files, it transforms each occurrence of String and Str into :"Elixir.String" .

All of this is important because it has everything to do with how our Elixir modules are compiled into beam files and with how module resolution happens within the runtime.

This can be further demonstrated by creating a file example.ex with the following two modules:

defmodule MyApp . User . Account do end defmodule Pokemon do end

Let's compile the file with elixirc and then see what the compiler gives us.

$ elixirc example.ex $ ls Elixir.MyApp.User.Account.beam Elixir.Pokemon.beam example.ex

Elixir and Erlang don't care much for the name of our files beyond ensuring they are loaded. When it comes to compiling the code, each module is compiled into its own file based on the fully expanded module name -- this means for our Elixir files Elixir will be tacked onto the beginning.

One last thing of note. We define modules with atoms. Generally, we use the camel case style syntax for this -- e.g. MyApp . If modules are defined with this specific kind of atom, what is stopping us from using the other style of atom syntax? In short, nothing. Let's do it.

defmodule :my_app do end

Compiling a file with this module definition will produce a BEAM file with the name my_app.beam . Notice that the Elixir bit is not there. That's because :my_app is the fully expanded form of that atom. This is what Erlang modules look like when compiled. The Elixir bit gives us a language-specific namespace for the code we write. Though there is nothing, besides naming collisions, stopping us from subverting this.

Elixir can sometimes feel like its own language, but it is important to remember that it is deeply embedded in Erlang and the BEAM VM. Module naming, compilation, and module resolution are all part of this story.

Sources:

Photo Credit: Dmitri Popov, https://unsplash.com/@dmpop?photo=mnFGmGiuupw