Modules
Modules
Modules allow us to organize functions into a namespace. In addition to grouping functions, they allow us to define named and private functions.
Let’s look at a basic example:
defmodule Example do
def greeting(name) do
"Hello #{name}."
end
end
{:module, Example, <<70, 79, 82, 49, 0, 0, 6, ...>>, {:greeting, 1}}
Example.greeting("Sean")
"Hello Sean."
It is possible to nest modules in Elixir, allowing you to further namespace your functionality:
defmodule Example.Greetings do
def morning(name) do
"Good morning #{name}."
end
def evening(name) do
"Good night #{name}."
end
end
{:module, Example.Greetings, <<70, 79, 82, 49, 0, 0, 8, ...>>, {:evening, 1}}
Example.Greetings.morning("Sean")
"Good morning Sean."
Module Attributes
Module attributes are most commonly used as constants in Elixir. Let’s look at a simple example:
defmodule Example do
@greeting "Hello"
def greeting(name) do
~s(#{@greeting} #{name}.)
end
end
It is important to note there are reserved attributes in Elixir. The three most common are:
-
moduledoc
— Documents the current module. -
doc
— Documentation for functions and macros. -
behaviour
— Use an OTP or user-defined behaviour.
Structs
Structs are special maps with a defined set of keys and default values. A struct must be defined within a module, which it takes its name from. It is common for a struct to be the only thing defined within a module.
To define a struct we use defstruct
along with a keyword list of fields and default values:
defmodule Example.User do
defstruct name: "Sean", roles: []
end
{:module, Example.User, <<70, 79, 82, 49, 0, 0, 8, ...>>, %Example.User{name: "Sean", roles: []}}
Let’s create some structs:
%Example.User{}
%Example.User{name: "Sean", roles: []}
%Example.User{name: "Steve"}
%Example.User{name: "Steve", roles: []}
%Example.User{name: "Steve", roles: [:manager]}
%Example.User{name: "Steve", roles: [:manager]}
We can update our struct just like we would a map:
steve = %Example.User{name: "Steve"}
%Example.User{name: "Steve", roles: []}
sean = %{steve | name: "Sean"}
%Example.User{name: "Sean", roles: []}
Most importantly, you can match structs against maps:
%{name: "Sean"} = sean
%Example.User{name: "Sean", roles: []}
As of Elixir 1.8 structs include custom introspection. To understand what this means and how we are to use it let us inspect our sean capture:
inspect(sean)
"%Example.User{name: \"Sean\", roles: []}"
All of our fields are present which is okay for this example but what if we had a protected field we didn’t want to include? The new @derive
feature let’s us accomplish just this! Let’s update our example so roles
are no longer included in our output:
defmodule Example.UserDerive do
@derive {Inspect, only: [:name]}
defstruct name: nil, roles: []
end
{:module, Example.UserDerive, <<70, 79, 82, 49, 0, 0, 8, ...>>, #Example.UserDerive}
Note: we could also use @derive {Inspect, except: [:roles]}
, they are equivalent.
With our updated module in place let’s take a look at what happens in iex
:
sean = %Example.User{name: "Sean"}
%Example.User{name: "Sean", roles: []}
inspect(sean)
"%Example.User{name: \"Sean\", roles: []}"
The roles
are excluded from output!
Composition
Now that we know how to create modules and structs let’s learn how to add existing functionality to them via composition. Elixir provides us with a variety of different ways to interact with other modules.
Alias
Allows us to alias module names; used quite frequently in Elixir code:
defmodule Sayings.Greetings do
def basic(name), do: "Hi, #{name}"
end
defmodule ExampleB do
alias Sayings.Greetings
def greeting(name), do: Greetings.basic(name)
end
# Without alias
defmodule ExampleC do
def greeting(name), do: Sayings.Greetings.basic(name)
end
{:module, ExampleC, <<70, 79, 82, 49, 0, 0, 6, ...>>, {:greeting, 1}}
If there’s a conflict between two aliases or we just wish to alias to a different name entirely, we can use the :as
option:
defmodule ExampleD do
alias Sayings.Greetings, as: Hi
def print_message(name), do: Hi.basic(name)
end
{:module, ExampleD, <<70, 79, 82, 49, 0, 0, 6, ...>>, {:print_message, 1}}
It’s even possible to alias multiple modules at once:
defmodule ExampleF do
alias Sayings.{Greetings, Farewells}
end
warning: unused alias Farewells
Ayanami-sTower/Elixier-Notebooks/Modules.livemd#cell:mwa42jievrssy5ic:2
warning: unused alias Greetings
Ayanami-sTower/Elixier-Notebooks/Modules.livemd#cell:mwa42jievrssy5ic:2
{:module, ExampleF, <<70, 79, 82, 49, 0, 0, 4, ...>>, [Sayings.Greetings, Sayings.Farewells]}
Import
If we want to import functions rather than aliasing the module we can use import
:
last([1, 2, 3])
error: undefined function last/1 (there is no such import)
Ayanami-sTower/Elixier-Notebooks/Modules.livemd#cell:szqnymaakwv7pady:1
import List
last([1, 2, 3])
3
Filtering
By default all functions and macros are imported but we can filter them using the :only
and :except
options.
To import specific functions and macros, we must provide the name/arity pairs to :only
and :except
. Let’s start by importing only the last/1
function:
import List, only: [last: 1]
first([1, 2, 3])
error: undefined function first/1 (there is no such import)
Ayanami-sTower/Elixier-Notebooks/Modules.livemd#cell:juajiodcaaw6yfmh:2
import List, only: [last: 1]
last([1, 2, 3])
3
If we import everything except last/1
and try the same functions as before:
import List, except: [last: 1]
first([1, 2, 3])
error: undefined function first/1 (there is no such import)
Ayanami-sTower/Elixier-Notebooks/Modules.livemd#cell:yzxo5tgquavie4gc:2
import List, except: [last: 1]
last([1, 2, 3])
error: undefined function last/1 (there is no such import)
Ayanami-sTower/Elixier-Notebooks/Modules.livemd#cell:jjdwxzwih2l5te6u:2
In addition to the name/arity pairs there are two special atoms, :functions
and :macros
, which import only functions and macros respectively:
import List, only: :functions
import List, only: :macros
List
require
We could use require
to tell Elixir you’re going to use macros from another module. The slight difference with import
is that it allows using macros, but not functions from the specified module:
defmodule ExampleG do
require SuperMacros
SuperMacros.do_stuff()
end
error: module SuperMacros is not loaded and could not be found
Ayanami-sTower/Elixier-Notebooks/Modules.livemd#cell:bh4sq2drtd6su56g:2: ExampleG (module)
If we attempt to call a macro that is not yet loaded Elixir will raise an error.
use
With the use
macro we can enable another module to modify our current module’s definition. When we call use
in our code we’re actually invoking the __using__/1
callback defined by the provided module. The result of the __using__/1
macro becomes part of our module’s definition. To get a better understanding how this works let’s look at a simple example:
defmodule Hello do
defmacro __using__(_opts) do
quote do
def hello(name), do: "Hi, #{name}"
end
end
end
{:module, Hello, <<70, 79, 82, 49, 0, 0, 7, ...>>, {:__using__, 1}}
Here we’ve created a Hello
module that defines the __using__/1
callback inside of which we define a hello/1
function. Let’s create a new module so we can try out our new code:
defmodule ExampleH do
use Hello
end
{:module, ExampleH, <<70, 79, 82, 49, 0, 0, 6, ...>>, {:hello, 1}}
If we try our code out in IEx we’ll see that hello/1
is available on the Example
module:
ExampleH.hello("Sean")
"Hi, Sean"
Here we can see that use
invoked the __using__/1
callback on Hello
which in turn added the resulting code to our module. Now that we’ve demonstrated a basic example let’s update our code to look at how __using__/1
supports options. We’ll do this by adding a greeting
option:
defmodule HelloB do
defmacro __using__(opts) do
greeting = Keyword.get(opts, :greeting, "Hi")
quote do
def hello(name), do: unquote(greeting) <> ", " <> name
end
end
end
{:module, HelloB, <<70, 79, 82, 49, 0, 0, 7, ...>>, {:__using__, 1}}
Let’s update our Example
module to include the newly created greeting
option:
defmodule ExampleJ do
use Hello, greeting: "Hola"
end
{:module, ExampleJ, <<70, 79, 82, 49, 0, 0, 7, ...>>, {:hello, 1}}
ExampleJ.hello("Sean")
"Hi, Sean"
These are simple examples to demonstrate how use works but it is an incredibly powerful tool in the Elixir toolbox. As you continue to learn about Elixir keep an eye out for use, one example you’re sure to see is use ExUnit.Case, async: true
.
Note: quote
, alias
, use
, require
are macros related to metaprogramming.