Modules are a way of grouping functions together in Elixir and as a way of namespacing. Normally, functions that are grouped in a module are related to one another.
We have been using modules such as String and Enum throughout this series so far, and touched on how to define a module in a previous section.
In this section we’re going to take a deeper dive into different module features in Elixir.
Versions
- Elixir 1.10.0
Module Basics
Define a Module
To define a module we use the defmodule
macro, followed by the module name and a do block.
defmodule Math do
#body
end
Modules use the CamelCase
convention in which each word is capitalized with no spaces or underscores.
Modules must also have unique names.
Module File Naming
File names for Elixir modules also follow the snake_case convention. For example, the Math module should, by convention live in the math.ex file.
You’ll likely need to nest some modules inside other namespaces such as with “contexts” in the Phoenix Framework.
In this case, if Accounts is the context and User is the module, you’d expect to have an “accounts” directory with the file “user.ex”:
- accounts
|- user.ex
File Extensions
You’ve probably seen .ex
and .exs
extensions mentioned in your Elixir journey. The difference is important so I thought it would be good to touch on here.
When executed, both file extensions will compile and load their modules into memory.
Elixir treats both files exactly the same way, the only difference being that .ex
files are compiled to bytecode on disk as .beam
files. .exs
files are not, and therefore more suitable for scripting as interpreted code.
ExUnit tests are a good example of when it makes sense to use .exs
files.
Because they’re interpreted, you don’t have to recompile every time you make a change to your tests.
Module Attributes
Module attributes are defined using the @
symbol. Let’s use some examples to see how they’re used.
As Annotations
They can annotate a module with information about how to use the module:
defmodule Math do
@moduledoc """
Provides math-related functions.
"""
@doc """
Calculates the sum of two numbers.
"""
def sum(a, b), do: a + b
end
Here, we’ve use the reserved attributes in Elixir: @moduledoc
and @doc.
As Constants
Module attributes can also be used as constants:
defmodule MyApp do
@app_detail %{name: "MyApp", version: "0.1.0"}
IO.inspect @app_detail
end
Notice there is no =
operator being used for assignment. At compile time this attribute is replaced by its value.
You can also call a function to assign a module attribute:
defmodule MyApp do
@service Application.get_env(:my_app, :email_service)
end
But there is something important to keep in mind. From the docs:
Be careful, however: functions defined in the same module as the attribute itself cannot be called because they have not yet been compiled when the attribute is being defined.
As Temporary Storage
An example of using module attributes as temporary storage can be found in the ExUnit framework which uses them as annotation and storage:
defmodule MyTest do
use ExUnit.Case
@tag :external
test "contacts external service" do
# ...
end
end
Tags in ExUnit are used to annotate tests which can be later used to filter which tests you’d like to run.
Module Directives
Elixir provides three lexically scoped directives: alias
, import
and require
in addition to a macro use. Let’s see how these can be used when working with modules.
alias
All modules in Elixir are compiled to Atoms.
Aliases are used in Elixir to shorten the syntax of referring to other modules.
To see how we’ve already been using aliases , let’s inspect the String module:
iex> to_string(String)
"Elixir.String"
The String module is an alias of :"Elixir.String"
iex(2)> :"Elixir.String".capitalize("tom")
"Tom"
Writing String is shorter and easier to remember.
Define an Alias
Let’s define an alias of String itself to show how it works:
iex> alias String, as: Str
iex> Str.capitalize("tom")
"Tom"
We can now use Str
as an alias of String
.
The as:
is optional. If it’s not provided, Elixir will use the last module. We can see this in Phoenix.
Phoenix Example
Phoenix organizes code into “contexts” and in default cases has a main Repo module that’s responsible for interactions with the database.
Here’s an example how aliases are used for an Accounts context:
defmodule MyApp.Accounts do
alias MyApp.Accounts.User
alias MyApp.Repo
def list_users do
Repo.all(User)
end
end
Because the Repo and User modules will be used several times throughout the Accounts module, the aliases reduce code duplication, improve readability and simplify changes that may be required.
Multiple Aliases
We can alias multiple modules from the same namespace. In our example, if we also needed an alias for MyApp.Accounts.UserIdentity
, we could extend the current alias:
defmodule MyApp.Accounts do
alias MyApp.Accounts.{User, UserIdentity}
...
end
Now we can access User
and UserIdentity
without having to type the full list of namespaces.
NOTE: Multiple aliases are organized alphabetically by convention.
require
We use the require directive to invoke “macros” from a given module.
Macros are a complex subject which we haven’t yet explored, but they’re basically extensions of Elixir syntax aka. Syntactic Extensions
Let’s use is_odd/1
as an example:
iex> Integer.is_odd(3)
** (CompileError) iex:1: you must require Integer before invoking the macro Integer.is_odd/1
is_odd/1
is defined as a macro in the Integer
module. In order to use it in another module, we’ll need to require Integer
.
iex> require Integer
Integer
iex> Integer.is_odd(3)
true
import
We can use the import directive to access functions from other modules without having to use the fully qualified name.
Whenever a module is “imported” it is also “required”.
To import String
and use it’s functions directly
iex> import String
String
iex> reverse("tom")
"mot"
Limiting Imported Functions
Importing all functions of a module can pollute the module scope and create conflicts with functions in the current module.
We can limit which functions are imported by passing some options to the import directive:
iex> import String, only: [reverse: 1]
String
iex> reverse("tom")
"mot"
iex> capitalize("tom")
** (CompileError) iex:8: undefined function capitalize/1
Here we only import the reverse function from String
in this format: [function_name: arity]
You can also use exclude:
if you want all but certain functions:
import String, exclude: [reverse: 1]
use
use
is another macro which requires a given module and calls the __using__
macro defined in that module.
It is commonly used to bring extra functionality from libraries into our modules.
Here’s an example of how it’s used in a Phoenix Application module:
defmodule MyApp.Application do
use Application
...
end
We’re not going to dive too deep into macros as it’s complex subject. We’re only touching on more basic concepts when working in Elixir.
For now this gives us basic handle on what’s happening behind the scenes when we see the use directive in the wild.
Wrapping Up
This wraps up our “Getting Started with Elixir” series. Thanks for following along!
We’ve explored many great features of Elixir and as we slowly uncover different parts of Elixir, we can start to see how everything fits together.
This foundation will empower us to confidently build applications using Phoenix.
We’ll continue diving into more complex topics as we go, but for now thanks for reading!