Skip to content

Latest commit

 

History

History
371 lines (246 loc) · 15.6 KB

File metadata and controls

371 lines (246 loc) · 15.6 KB
layout title guide
getting_started
3. Modules
3

3 Modules

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.

3.1 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.

3.2 Scripted mode

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.

3.3 Functions and private functions

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'

3.4 Recursion

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)

3.5 Directives

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.

3.5.1 alias

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.

3.5.2 require

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.

3.5.3 import

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.

3.6 Module attributes

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.

3.7 Nesting

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.

3.7 Aliases

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 a Foo.Bar.Baz module is defined or not.

Note: preferably, the alias List would be converted to the atom Elixir.List instead of Elixir-List, however there is currently a limitation in Erlang that does not allow us to use such atoms.