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