layout | title | guide |
---|---|---|
getting_started |
3. Modules |
3 |
In Elixir, you can group several functions into a module. In the previous chapter, for example, we invoked functions from the module List:
iex> List.flatten [1,[2],3]
[1, 2, 3]
In order to create our own modules in Elixir, all we have to do is to call the defmodule
function and use def
to define our functions:
iex> defmodule Math do
...> def sum(a, b) do
...> a + b
...> end
...> end
iex> Math.sum(1, 2)
3
Before diving into modules, let's first have a brief overview about compilation.
Most of the time it is convenient to write modules into files so they can be compiled and reused. Let's assume we have a file named math.ex
with the following contents:
defmodule Math do
def sum(a, b) do
a + b
end
end
This file can be compiled using elixirc
(remember, if you installed Elixir from a packaged or compiled it, elixirc
will be inside the bin directory):
elixirc math.ex
Which will then generate a file named Math.beam
containing the bytecode for the defined module. Now, if we start iex
again, our module definition will be available (considering iex
is being started in the same directory the bytecode file is):
iex> Math.sum(1, 2)
3
Elixir projects are usually organized into three directories:
- ebin - contains the compiled bytecode
- lib - contains elixir code (usually
.ex
files) - test - contains tests (usually
.exs
files)
In many cases, since the bytecode is in ebin
, you need to explicitly tell Elixir to look for code in the ebin
directory:
iex -pa ebin
Where -pa
stands for path append
. The same option can also be passed to elixir
and elixirc
executables. You can execute elixir
and elixirc
without arguments to get a list of options.
In addition to the Elixir file .ex
, Elixir also supports .exs
files for scripting. Elixir treats both files exactly the same way, the only difference is in intention. .ex
files are meant to be compiled while .exs
files are used for scripting, without the need for compilation. For instance, one can create a file called math.exs
:
defmodule Math do
def sum(a, b) do
a + b
end
end
IO.puts Math.sum(1, 2)
And execute it as:
elixir math.exs
The file will be compiled in memory and executed, printing 3 as the result. No bytecode file will be created.
Inside a module, we can define functions with def
and private functions with defp
. A function defined with def
is available to be invoked from other modules while a private function can only be invoked locally.
defmodule Math do
def sum(a, b) do
do_sum(a, b)
end
defp do_sum(a, b) do
a + b
end
end
Math.sum(1, 2) #=> 3
Math.do_sum(1, 2) #=> ** (UndefinedFunctionError)
Function declarations also support guards and multiple clauses. If a function has several clauses, Elixir will try each clause until it finds one that matches. Here is the implementation of a function that checks if the given number is zero or not:
defmodule Math do
def zero?(0) do
true
end
def zero?(x) when is_number(x) do
false
end
end
Math.zero?(0) #=> true
Math.zero?(1) #=> false
Math.zero?([1,2,3])
#=> ** (FunctionClauseError)
Giving an argument that does not match any of the clauses raises an error.
Named functions also support default arguments:
defmodule Funs do
def join(a, b, sep // ' ') do
List.wrap(a) ++ sep ++ List.wrap(b)
end
end
IO.puts Funs.join('Hello', 'world') #=> Hello world
IO.puts Funs.join('Hello', 'world', '_') #=> Hello_world
When using default values, one must be careful to avoid overlapping function definitions. Consider the following example:
defmodule Funs do
def join(a, b) do
IO.puts "***First join"
List.wrap(a) ++ List.wrap(b)
end
def join(a, b, sep // ' ') do
IO.puts "***Second join"
List.wrap(a) ++ sep ++ List.wrap(b)
end
end
If we save the code above in a file named "test_funs.ex" and compile it, Elixir will emit the following warning:
test_funs.ex:7: this clause cannot match because a previous clause at line 2 always matches
The compiler is telling us that invoking the join
function with two arguments will always choose the first definition of join
whereas the second one will only be invoked when three arguments are passed:
$ iex test_funs.ex
iex> Funs.join 'Hello', 'world'
***First join
'Helloworld'
iex> Funs.join 'Hello', 'world', ' '
***Second join
'Hello world'
Due to data structure immutability, loops in Elixir (and in functional programming languages) are written differently from conventional imperative languages. For example, in an imperative language, one would write:
for(i = 0; i < array.length; i++) {
array[i] = array[i] * 2
}
In the example above, we are mutating the array which is not possible here. Therefore, in functional languages we use recursion; a function is called recursively until a condition is reached. Consider the example below that manually sums all the items in the list:
defmodule Math do
def sum_list([h|t], acc) do
sum_list(t, h + acc)
end
def sum_list([], acc) do
acc
end
end
Math.sum_list([1,2,3], 0) #=> 6
In the example above, we invoke sum_list
giving a list [1,2,3]
and the initial value 0
as arguments. As we saw in the previous chapter, when a function has many clauses, we will try each clause until we find one that matches according to the pattern matching rules. In this case, the list [1,2,3]
matches against [h|t]
which assigns h = 1
and t = [2,3]
while acc
is set to 0.
Then, we add the head of the list to the accumulator h + acc
and call sum_list
again, recursively, passing the tail of the list as argument. The tail will once again match [h|t]
until the list is empty, as seen below:
sum_list [1,2,3], 0
sum_list [2,3], 1
sum_list [3], 3
sum_list [], 6
When the list is empty, it will match the final clause which returns the final result of 6
. In imperative languages, such implementation would usually fail for large lists because the stack (in which our execution path is kept) would grow until it reaches a limit. Erlang, however, does last call optimization in which the stack does not grow when a function exits by calling another function.
Recursion and last call optimization are an important part of Erlang and are commonly used to create loops, especially in cases where a process needs to wait and respond to messages (using the receive
macro we saw in the previous chapter). However, recursion as above is rarely used to manipulate lists, since the Enum
module already abstracts such use cases. For instance, the example above could be simply written as:
Enum.reduce([1,2,3], 0, fn(x, acc) -> x + acc end)
In order to facilitate software reuse, Elixir supports three directives. As we are going to see below, they are called directives because they are the only functions in Elixir that have lexical scope.
alias
allows you to setup aliases for any given module name. For instance, one can do:
defmodule Math do
alias MyList, as: List
end
And now, any reference to List
will be automatically replaced by MyList
. In case one wants to access the original List
, it can be done by accessing the module directly via Elixir
:
List.values #=> uses MyList.values
Elixir.List.values #=> uses List.values
Calling alias
without an as
option sets the alias automatically to the last part of the module name, for example:
alias Foo.Bar.Baz
Is the same as:
alias Foo.Bar.Baz, as: Baz
Notice that alias
is lexically scoped, which allows you to set aliases inside specific functions:
defmodule Math do
def add(a, b) do
alias MyList, as: List
# ...
end
def minus(a, b) do
# ...
end
end
In the example above, since we are invoking alias
inside the function add
, the alias will just be valid inside the function add
. minus
won't be affected at all.
In general, a module does not need to be required before usage, except if we want to use the macros available in that module. For instance, suppose we created our own my_if
implementation in a module named MyMacros
. If we want to invoke it, we need to first explicitly require MyMacros
:
defmodule Math do
require MyMacros
MyMacros.my_if do_something, it_works
end
An attempt to call a macro that was not loaded will raise an error. Note that like the alias
directive, require
is also lexically scoped.
We use import
whenever we want to easily access functions or macros from other modules without using the qualified name. For instance, if we want to use the duplicate
function from List
several times in a module and we don't want to always type List.duplicate
, we can simply import it:
defmodule Math do
import List, only: [duplicate: 2]
def some_function do
# call duplicate
end
end
In this case, we are importing only the function duplicate
(with arity 2) from List
. Although only:
is optional, its usage is recommended. except
could also be given as an option.
If we want to import only :functions
or :macros
from a given module, we can also pass a first argument selecting the scope:
import :macros, MyMacros
We can then use only
or except
to filter the macros being included. Finally, note that import
is lexically scoped, this means we can import specific macros inside specific functions:
defmodule Math do
def some_function do
import List, only: [duplicate: 2]
# call duplicate
end
end
In the example above, the imported List.duplicate
is only visible within that specific function. duplicate
won't be available in any other function in that module (or any other module for that matter).
Note that importing a module automatically requires it. Furthermore, import
also accepts the as:
option which is automatically passed to alias
in order to create an alias.
Elixir brings the concept of module attributes from Erlang with some differences. The canonical example for attributes is annotating that a module implements an OTP behavior, for example gen_server
:
defmodule MyServer do
@behavior :gen_server
# ... callbacks ...
end
Now if the module above does not implement any of the callbacks required by the :gen_server
behavior, a warning will be raised. Another attribute used internally by Elixir is @vsn
:
defmodule MyServer do
@vsn 2
end
@vsn
refers to the module version and is used by the code reloading mechanism to check if a module has been updated or not. If no version is specified, the version is set to the MD5 checksum of the module functions.
Elixir has a handful of reserved attributes. The following are currently functional in Elixir:
@behaviour
and@behavior
- used for specifying an OTP or user-defined behavior;@vsn
- used for specifying the module version;@compile
- provides options for the module compilation;@moduledoc
- provides documentation for the current module;@doc
- provides documentation for the function that follows it;@file
- changes the filename in stacktraces of the next defined function;@on_load
- expects a function name that will be invoked whenever the module is loaded. The function must have arity 0 and has to return:ok
, otherwise the loading of the module is aborted;@before_compile
- expects a{ module, function }
that will be invoked with the module name before the module is compiled. The function may be a macro, allowing you to inject functions inside the module exactly before compilation;@after_compile
- expects a{ module, function }
that will be invoked with the module name and its object code right after the module is compiled and loaded;
The following attributes are part of typespecs and are also supported by Elixir:
@spec
- provides a specification for a function;@callback
- provides a specification for the behavior callback;@type
- defines a type to be used in @spec;@export_type
- informs which types can be exported;@opaque
- defines an opaque type to be used in @spec;
In addition to the built-in attributes outlined above, customer attributes may also be added:
defmodule MyServer do
@my_data 13
IO.inspect @my_data #=> 13
end
Unlike Erlang, user defined attributes are not stored in the module by default since it is common in Elixir to use such attributes to store temporary data. A developer can configure an attribute to behave closer to Erlang by calling Module.register_attribute/2
.
Finally, notice that attributes can also be read inside functions:
defmodule MyServer do
@my_data 11
def first_data, do: @my_data
@my_data 13
def second_data, do: @my_data
end
MyServer.first_data #=> 11
MyServer.second_data #=> 13
Notice that reading an attribute inside a function takes a snapshot of its current value. In other words, the value is read at compilation time and not at runtime. Check the documentation for the module Module
documentation for other functions to manipulate module attributes.
Modules in Elixir can be nested too:
defmodule Foo do
defmodule Bar do
end
end
The example above will define two modules Foo
and Foo.Bar
. The second can be accessed as Bar
inside Foo
as long as they are in the same scope. If later the developer decides to move Bar
to another file, it needs to be referenced by its full name (Foo.Bar
) or an alias needs to be set using the alias
directive discussed above.
In Erlang (and consequently in the Erlang VM), modules and functions are represented by atoms. For instance, this is valid Erlang code:
Mod = lists,
Mod:flatten([1,[2],3]).
In the example above, we store the atom lists
in the variable Mod
and then invoke the function flatten
in it. In Elixir, the same idiom is allowed. In fact, we could call the same function flatten
in lists
as:
iex> :lists.flatten([1,[2],3])
[1,2,3]
This mechanism is exactly what empowers Elixir aliases. An alias in Elixir is a capitalized identifier (like List
, Keyword
, etc) which is converted to an atom representing a module during compilation. For instance, by default List
translates to the atom Elixir-List
:
iex> is_atom(List)
true
iex> to_binary(List)
"Elixir-List"
Given a scope, aliases can also be set using the alias
directive discussed above. For instance, it is particularly useful when interacting with Erlang code:
alias :application, as: Application
This allows you to write code transparently without using the Erlang notation to access the application
module.
Note: an alias does not actually ensure the aliased module really exists. For instance,
Foo.Bar.Baz
will return an atom regardless if aFoo.Bar.Baz
module is defined or not.
Note: preferably, the alias
List
would be converted to the atomElixir.List
instead ofElixir-List
, however there is currently a limitation in Erlang that does not allow us to use such atoms.