Getting Started with Elixir - Modules

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.
Getting Started with Elixir - Modules

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!

Troy Martin
Senior Engineer working with Elixir and JS.
Keep in the Loop

Subscribe for Updates