Elixir Course Online | Prograils - Software Development Company
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:
Pass the file name as iex argument:
➜ iex math.exs
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
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
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,2,3], &IO.puts/1 -> it prints 1.
[2,3], &IO.puts/1 -> it prints 2.
[3], &IO.puts/1 -> it prints 3.
[], &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)