Want to learn?

Modules and named functions

File compilation

Before we start with modules and named functions we should get back to file compilation for a second. Let's assume you have math.exs file which declares a Math module. You want to use this module inside of IEx to test it out. How to do that? There are two ways:

  1. Pass the file name as iex argument:

    ➜ iex math.exs
    
  2. Compile and load the file inside of IEx with c helper (c stands for compile):

    iex> c "math.exs"
    

From now on, we will declare multi-line modules and functions which are hard to fix in case of a typo in IEx. It's better to use one of the above techniques of file compilation and load the code from that file.

Modules

There are numerous libraries written in Elixir, and each of them may declare functions with the same name. Sooner or later you may find yourself in a situation of naming conflict. You can deal with this issue by structuring your code in modules. In fact, Elixir named functions must be written inside modules.

Declaration

Documentation

We've already used modules in previous chapters. Let's recall one use case:

Enum.map array, fn num -> num * 2 end

We used map/2 named function declared inside of Enum module which holds logic capable of operating on an enumerable data type. Whenever you declare functions operating on a similar data type or in a similar way, you should create a module and put logic into it. In other words, a module is just a container.

Let's declare our first module:

defmodule Calculator do
  def sum(a, b) do
    a + b
  end
end

We define a module with defmodule macro. In this case, module is named Calculator. Internally, module names are just atoms. When you write a name starting with an uppercase letter, such as Calculator in our case, Elixir converts it internally into an atom called Elixir.Calculator.

iex> is_atom Calculator
true
iex> to_string Calculator
"Elixir.Calculator"
iex> Calculator == :"Elixir.Calculator"
true

Knowing that, we can call sum function in two ways (first is preferred):

iex> Calculator.sum(1, 2)
3
iex> :"Elixir.Calculator".sum(1, 2)
3

When calling a function of given module, you should follow this structure:

ModuleName.method_name

As you can see, invocation of the named function is always preceded by module name (within which it's declared), and . dot.

Nesting modules

Modules can be nested:

defmodule OuterModule do
  defmodule InnerModule do
    def func do
    end
  end
end

# in IEx

iex> OuterModule.InnerModule.func
nil

You can declare a nested module by putting it inside of another module declaration and then access it with the . operator. Module nesting is, in fact, another syntactic sugar. You can rewrite it this way:

defmodule OuterModule.InnerModule do
  def func do
  end
end

When we define a module inside another, Elixir prepends the outer module name to the internal module name, putting a dot between the two.

Module alias

Sometimes module names can be pretty long, especially when we're dealing with nested structure. To solve that issue, you can create an alias for a module:

defmodule One do
  defmodule Two do
    defmodule Three do
      def hello do
        IO.puts "hello"
      end
    end
  end
end

# in IEx

iex> alias One.Two.Three, as: Three
One.Two.Three
iex> Three.hello
hello
:ok

You can declare alias with alias macro, which takes optional parameter as: NewModuleName where NewModuleName is the alias itself. If you don't specify as parameter, by default alias will inherit name from the last module, in this case, Three:

iex> alias One.Two.Three
One.Two.Three
iex> Three.hello
hello
:ok

Module Attributes

A module can have attributes which act as metadata:

defmodule Math do
  @pi 3.14
end

You can assign a value to attribute with the following syntax:

@name value

In the Math module we defined @pi attribute which holds 3.14 value. This attribute is specific to a module within which it's declared (cannot be accessed elsewhere). You can access attributes in named functions declared within the same module. However, when you want to change an attribute value, you can only do that in the scope of a module - not inside of a function.

This will work:

defmodule Math do
  @pi 3.14 # @pi is from now on equal to 3.14
  @pi 3.14159265 # @pi is re-declared with value 3.14159265
end

This won't work:

defmodule Math do
  @pi 3.14
  def func do
    @pi 3.14159265
  end
end

** (ArgumentError) cannot set attribute @pi inside function/macro

Extending modules

Once you have module declared, you can extend it:

defmodule Math do
  @pi 3.14

  def pi do
    @pi
  end
end

defmodule Math do # Extending
  @euler 2.72

  def euler do
    @euler
  end
end

# in IEx
iex> Math.pi
3.14
iex> Math.euler
2.72

Named functions

Declaration

Documentation

Let's get back to our example of sum function:

defmodule Calculator do
  def sum(a, b) do
    a + b
  end
end

We already know the structure of the module declaration. What we have not yet covered is a sum function. Let's break it down:

  • declaration starts with def keyword;

  • name of a function should be written in snake case;

  • you can declare multiple (or none) arguments of a function in brackets;

  • after brackets, you have to provide do ... end block;

  • in the block, you declare the body of a function.

You should notice that named functions are just like anonymous functions, but with the name assigned.

Guard clauses

You can use the same guard clauses in named functions as in anonymous functions:

defmodule Calculator do
  def divide(a, b) when b != 0 do
    a / b
  end
end

Let's execute it:

iex> Calculator.divide(5,1)
5.0
iex> Calculator.divide(5, 0)
** (FunctionClauseError) no function clause matching in Calculator.divide/2

    The following arguments were given to Calculator.divide/2:

        # 1
        5

        # 2
        0

    iex:5: Calculator.divide/2

Arity

In Elixir, a named function is identified by module, name and number of parameters (arity). Let's see what happens when we execute sum function with the wrong number of arguments:

iex> Calculator.sum(5)
** (UndefinedFunctionError) function Calculator.sum/1 is undefined or private. Did you mean one of:

      * sum/2

    Calculator.sum(5)

As you can see, an error is raised. We can observe that Elixir is looking for:

Calculator.sum/1

which is sum function declared withing Calculator module with arity 1. It can't find it since there is only function with arity 2:

Did you mean one of:

      * sum/2

Multiple functions with the same name

Since arity is part of a named function definition, we can create multiple functions with the same name but a different number of arguments:

defmodule Calculator do
  def sum(_) do
    IO.puts "You need to specify at least two arguments"
  end

  def sum(a, b) do
    a + b
  end

  def sum(a, b, c) do
    a + b + c
  end

  def sum(a, b, c, d) do
    a + b + c + d
  end
end

# in IEx

iex> Calculator.sum(1)
You need to specify at least two arguments
:ok
iex> Calculator.sum(1, 2)
3
iex> Calculator.sum(1, 2, 3)
6
iex> Calculator.sum(1, 2, 3, 4)
10

There are a couple of things to note here:

  • each of these functions is different for Elixir because of their rarity,

  • function evaluation starts from the top,

  • function with the same arity will be executed.

But, there is more than that. In the previous chapter, we covered how anonymous functions use pattern matching to bind their parameter list to the passed arguments. It works the same way with named functions. We are going to use list iterator to illustrate how it works:

defmodule Collection do
  def each([], _func) do
  end

  def each([h | t], func) when is_function(func) do
    func.(h)
    each(t, func)
  end
end

Let's break it down:

  • both each function declarations take two arguments: an array and anonymous function,

  • first declaration matches when an empty array is passed as a first argument,

  • _func means that we don't care what is the second argument as long as the first one is an empty array,

  • the second declaration assumes that non-empty array and a function are passed,

  • [h | t] uses pattern matching to extract head and tail from an array,

  • we use is_function(func) guard clause to make sure that the second argument is a function,

  • func.(h) executes a function with a head of given array,

  • each(t, func) calls the same function and sets tail as an array.

Let's see how it works in action:

iex> Collection.each [1,2,3], &IO.puts/1
1
2
3
nil

In this case, each function was executed four times with following arguments:

  1. [1,2,3], &IO.puts/1 -> it prints 1.

  2. [2,3], &IO.puts/1 -> it prints 2.

  3. [3], &IO.puts/1 -> it prints 3.

  4. [], &IO.puts/1 -> it doesn't print anything, since each([], _func) is called.

In Elixir, recursion and pattern matching against different function arguments are used a lot. You will see it quite often. Remember when we noted in the control flow chapter that "in Elixir, we should use these statements as rarely as possible"? It's because you can declare multiple function definitions and control the flow with arguments pattern matching. It brings more clarity and less complexity.

Block

You should already notice that both module and function requires do...end block. In Elixir, it's one way of grouping expressions and passing them to other code. This syntax is only syntactic sugar. There are two ways of declaring function block:

  • multi-line module and function:

    defmodule Calculator do
      def power(number) do
        number * number
      end
    end
    
  • one-line module and function:

    defmodule Calculator do
      def power(number), do: number * number
    end
    
    # or
    
    defmodule Calculator, do: (def power(number), do: number * number)
    

The "new" thing is a one-line version which is in fact the primary way of passing a block. During compilation, multi-line version of the block is translated into one-line do: ... version. When it comes to functions, one-line version might be convenient. In case of modules, it's much better to use multi-line version.

Default parameters

In many programming languages, it's possible to assign a default value to function parameters. Elixir is no different. Let's examine the following function:

defmodule Screen do
  def draw_line(opts \\ []) do
    IO.inspect opts
  end
end

Screen module has draw_line/1 function which has one thing that may look different to you:

opts \\ []

\ operator defines a default value for opts variable. Any expression is allowed to serve as a default value, but it won’t be evaluated during the function definition. In this case, we want opts to store an empty array when no value is passed.

iex> Screen.draw_line
[]
iex> Screen.draw_line(width: 50)
[width: 50]

Function variables are bound to values given to it via pattern matching. Pattern matching fails when the number of values on both sides is not equal. That's why you can't invoke a method with the wrong number of arguments. However, as you already know, we can declare default values for arguments. Let's take a look at this function:

defmodule Example do
  def func(first, second \\ 0, third \\ 0, fourth) do
    [first, second, third, fourth]
  end
end

There are two arguments with default value: second and third. Let's see what happens when we call this function with different number of arguments:

iex> Example.func(1, 1, 1, 1)
[1, 1, 1, 1]
iex> Example.func(1, 1, 1)
[1, 1, 0, 1]
iex> Example.func(1, 1)
[1, 0, 0, 1]
iex> Example.func(1)
** (UndefinedFunctionError) function Example.func/1 is undefined or private. Did you mean one of:

      * func/2
      * func/3
      * func/4

    Example.func(1)

When you pass four arguments all of them match to value 1. However, when you pass fewer arguments, three, for instance, the last argument with a default value will be bound to 0. Arguments with a default value are evaluated from end to beginning of a list. The actual rule for passing a minimum number of values to function is the following:

(number of arguments) - (number of arguments with default values)

Private functions

Functions can also be private - one that can be called only within the module that declares it.

defmodule Example do
  def func, do: pfunc
  defp pfunc, do: IO.puts "Hello from private function"
end

# in IEx

iex> Example.func
Hello from private functions
:ok
iex> Example.pfunc
** (UndefinedFunctionError) function Example.pfunc/0 is undefined or private. Did you mean one of:

      * func/0

    Example.pfunc()

As you can see, we can call the private function inside of a public function, but we can't do it publicly via the module. There is also one limitation when it comes to private functions - they can't have the same name and arity as functions inside the same module:

defmodule Example do
  def func
  defp func
end

# in IEx

** (CompileError) iex:3: defp func/0 already defined as def

Function capturing

We are already familiar with name/arity function notation. You can use this notation to capture a function and bind it to variable. Let's examine this example:

iex> func = &String.length/1
&String.length/1
iex> func.("Hello world")
11

Capture operator (&) allows named functions to be assigned to variables and passed as arguments in the same way we assign, invoke and pass anonymous functions. Since we treat such a function as anonymous, you have to invoke it via .().

Pipe operator

In object oriented programming languages it's very easy to chain functions call on an object:

data.fun1().fun2().fun3().fun4()

Imagine you want to perform similar functions call on a data in Elixir:

fun4(fun3(fun2(fun1(data))))

Well, it doesn't look like readable code, does it? You don't have to worry about that since there is a solution for this issue - pipe operator. The same functions call can be rewritten this way:

data |> func1 |> func2 |> func3 |> func4

Elixir is all about transforming data. Pipe operator illustrates it perfectly. Let's break it down:

  • we start with data,

  • >` means that returned value of an expression on the left side of this operator will be passed as the first argument of a function on the right side,

  • func1 will be called with data attribute: func1(data),

  • the result of func1(data) will be passed as the first argument of func2,

  • execution of functions will continue until it reaches func4.

In short, these two expressions are equal:

fun4(fun3(fun2(fun1(data)))) == data |> func1 |> func2 |> func3 |> func4

You can also chain functions call in multi-line:

transformed_data =
  data
  |> func1
  |> func2
  |> func3
  |> func4

When one-line version affects the readability of code, you should always use multi-line version.

To help you understand pipe operator let's write a function that:

  • takes number higher than 0 as an argument,

  • creates an array containing a range of elements: 0..argument,

  • converts each element to a string,

  • joins all string elements to one string.

defmodule DataTransformator do
  def range_to_string(num) when is_integer(num) and num > 0 do
    0..num
    |> Enum.to_list
    |> Enum.map(&Integer.to_string/1)
    |> Enum.join
  end
end

# in IEx

iex> DataTransformator.range_to_string(10)
"012345678910"

Range 0..num is converted into list with Enum.tolist/1. List of integer values is then transformed with Enum.map(&Integer.tostring/1) into a list of strings. Notice that we pass Integer.to_string/1 function as anonymous. In the end, elements of the array are merged into one string. There is also an important thing - you need parentheses when piping into a function call.

It means that this would not work:

|> Enum.map &Integer.to_string/1

but this will:

|> Enum.map(&Integer.to_string/1)
Check our latest product - it's based on our experience of managing over 50-people strong company. The tool we're missing as a small company and not an enterprise.

humadroid.io is an employee and performance management software. It's an unique tool allowing everyone to be in the loop - by having up to date info about co-workers, time-off, benefits, assets, helping with one-on-ones, being a go-to place for company-wide announcements.

Check out humadroid.io
Top

Contact us

* Required fields

The controller of your personal data provided via this contact form is Prograils sp. z o.o., with a registered seat at Sczanieckiej 9A/10, 60-215 Poznań. Your personal data will be processed in order to respond to your inquiries and for our marketing purposes (e.g. when you ask us for our post-development, maintenance or ad hoc engagements for your app). You have the rights to: access your personal data, rectify or erase your personal data, restrict the processing of your personal data, data portability and to object to the processing of your personal data. Learn more.

Notice

We do not track you online. We use only session cookies and anonymous identifiers for the purposes specified in the cookie policy. No third-party trackers.

I understand
Elo Mordo!Elo Mordo!