Mappa Via Marconi 20, Bussolengo (VR)
Email info@devinterface.com

Introduction to Elixir Part 2

Elixir logo and article's title

Index

In our previous article, we discussed Elixir, explaining why we decided to adopt this technology and introducing a number of topics such as functional programming, recursion, pattern matching, immutability and the actor model. 

In this second part, we will focus on some crucial aspects of Elixir: data types and how to handle them, the use of variables and modules, functions and their peculiarities, and finally advanced pattern matching.

 

Data types in Elixir

All data types in Elixir are immutable, so they are constants.

Atoms

In Elixir, an atom is a constant whose name is directly the value itself. Atoms are used to uniquely identify elements. For example, an atom might look like this:

:example_atom

If there is a space inside the name of an atom we can write it like this: :

‘Example Atom’.

Lets take a closer look at the world of atoms in Elixir. Imagine a logo of a very famous brand. You will immediately associate the logo with the brand name. Whether you are shown the symbol or the brand name, both point to the same thing. Let us take a concrete example. The following code is not a valid syntax for Elixir but we want you to understand what we mean by atom:

var apple = ‘apple’

In this case we have a variable called apple and the value assigned to the variable is also ‘apple’. As you can see, the value and the name of the variable are identical and this is what an atom represents and we can represent it like this:
:apple

Strings

In Elixir, strings are represented by a pair of double inverted commas and may contain a sequence of characters.
"Devinterface"

On the other hand, if we use single quotes (') instead of double quotes, we get a character list which is a list of Unicode characters. This difference is important, as character lists and strings are treated differently within the language.

Numbers

Elixir supports both integers and floats.

  • Integers: Integers in Elixir can be positive or negative and have no fixed size limit.
  • Floats: Floating point numbers are written with a decimal point.

Lists 

In Elixir, lists are collections of elements that can be of different types. Lists are implemented as linked lists, which means that each list element contains a value and an implicit reference to the next element. Characteristics:

  • Labelled: each list element can be of any type, including other lists, strings, numbers and maps.
  • Linked lists: internally, lists are represented as a series of nodes, where each node contains a value and a pointer to the next node. The list ends with a special element, the empty list [], which indicates the end of the collection.

Tuples

Tuples are collections that are stored contiguously in memory, allowing fast access to their elements by index. They are common in functions, results and messages sent to processes in Elixir's core and community libraries. They are often used to pass a signal with values.

Maps

Maps are collections of key-value pairs. Each key in a map is unique and can be of any data type, although atoms are frequently used for keys because of their simplicity and readability. 

 

Variables and modules

Values

Values are everything that can represent data in Elixir, i.e. what a programme receives as input, calculates and generates as a result.

Open your IEx shell and type this command:
 iex> 2
 2

You have typed a value of type integer, which represents an integer number. Let's try another type of value. Try typing this value into your IEx shell:

iex> "Programming in Elixir is fun"
"Programming in Elixir is fun"

The text enclosed in double quotes is a value of type String.t, which represents a string of characters.

The following table shows some of the types you will find in Elixir, their uses and some examples to try out in your IEx shell:

Tabella con 9 tipologie di variabili e spiegazione uso con esempi

Source: Almeida U. (2019) "Learn Functional Programming with Elixir" di Ulisses Almeida, p. 12

 

Operators

If we type:

iex> 5 - 3
2

5 is a value and - is an operator. Operators calculate values and generate a result. You can also combine several operators and values:

iex> (4 - 1) * 5
15

All operators are executed in a particular order, called precedence. For example, * has a precedence greater than +. In an expression that contains both operators, the * operator will be executed first. Parentheses can be used to change precedence. Expressions inside parentheses come first. Here are some of the most common operators in Elixir:

Tabella degli operatori Elixir

Source: Almeida U. (2019) "Learn Functional Programming with Elixir" di Ulisses Almeida, p. 14

Variables

Variables are containers of values. In Elixir, variables are assigned using the = operator. Variables in Elixir are immutable, which means that once a value has been assigned to a variable, that value cannot be changed. However, it is possible to re-use the same variable name to assign a new value.

The conventions of the Elixir community must be used when naming variables. Variables follow the snake_case format. This means that variable names must be lower case and compound names must have the separator ‘_’ . Names beginning with a capital letter are used in forms instead. 

iex> x = 8
8

iex> x = x + 2
10
 

Modules

Modules in Elixir are used to organise code into logical units. Elixir provides many useful modules and each of them is explained online in the official documentation. A module may contain functions, macros and attributes. Here are the most common ones:

Elenco moduli Elixir più comuni

Source: Almeida U. (2019) "Learn Functional Programming with Elixir" di Ulisses Almeida, p. 25

As we mentioned in the previous section, the CamelCase name format is used for forms. Each word of the compound name must begin with a capital letter.

 

Functions

Functions receive an input, perform calculations and return an output. The function body is where expressions are written to perform a calculation. The last value of the expression in the function body is the function's output. In Elixir, we have several types of functions, so here is an overview. . 

Anonymous functions 

Anonymous functions do not have a name and must be bound to a variable in order to be reused. They are useful for creating functions on the fly and are defined using the fn and end syntax.

Named functions

In Elixir, named functions are defined within modules. An atom or alias can be used to name a module. An alias in Elixir is any word that begins with a capital letter and only ASCII characters are allowed.

Higher-order functions 

In this case, the higher-order functions are functions that accept other functions as arguments. They play an important role in many Elixir functions and libraries and are extremely effective in creating useful functions with a simple interface.

Predefined functions

Predefined functions are part of the Kernel module, available in all modules without the need for import.

Private functions

Private functions are useful for controlling the accessibility of functions from outside and cannot be imported from other modules. They are defined using the defp keyword.

Callback functions

Callback functions are functions that are passed as arguments to higher-order functions and called at a later time. 

Recursive functions

A recursive function is when a function calls itself, leading to subsequent calls of the same function. 

 

Advanced pattern matching

In the previous article, we explored the basic concepts of pattern matching in Elixir. Using the = operator for pattern matching, we can check whether both sides of the operator have a match. We will now delve into some advanced applications of this powerful feature, seeing how to deconstruct complex data and use pattern matching in combination with other Elixir features.

Parts of a string

Elixir allows pattern matching directly on binaries, which can represent strings. To check the beginning of a string, we can use the <> operator. It is important to remember that, for strings, we cannot use a variable on the left-hand side of the <> operator. Let us see an example:

iex> "Hello, " <> rest = "Hello, world"
iex> rest
"world"

In this example, we have correctly extracted rest from the string. However, things change if we try to do the following with a variable at the beginning:

iex> prefix <> " world" = "Hello, world"
** (CompileError) a binary field without size is only allowed at the end of a binary pattern and never allowed in binary generators

Strings are binary and <> is a binary operator. The error indicates that we cannot start the expression with a variable without specifying its binary size: we cannot control the end of a string in this way. We can get around this problem by inverting the string and using the <> operator in combination with String.reverse:

iex> reversed_string = String.reverse("Hello, world")
iex> "dlrow " <> reversed_rest = reversed_string
iex> String.reverse(reversed_rest)
"Hello,"

This way, we compared the inverted string and extracted the prefix in a variable. However, this is an unusual solution, so it is preferable to use regular expressions. Please refer to the official documentation instead to learn more about binary pattern matching

Tuples

Tuples are collections that are stored contiguously in memory, allowing fast access to their elements by index.  Let us look at an example:

iex> {language, level} = {"Elixir", "Intermediate"}

This expression assumes that the right term is a tuple of two elements. When the expression is evaluated, the variables language and level are assigned to the corresponding elements of the tuple. Let us check its correctness:

iex> language
"Elixir"

iex> level
"Intermediate"

This technique is particularly useful when calling a function that returns a tuple and you wish to assign the individual elements of the tuple to separate variables. The following example calls the function File.stat/1 to obtain information about a file:

iex> {:ok, file_info} = File.stat("path/to/file")

The file information is contained in a tuple that we can further deconstruct:

iex> %{size: size, access: access} = file_info
iex> size
1024

iex> access
:read_write

But what happens if the right-hand side does not match the pattern? The match fails and we get an error:

iex> {language, level} = "can't match"
** (MatchError) no match of right hand side value: "can't match"

This error indicates that the string on the right-hand side does not match the expected structure of a two-element tuple.

Lists

In Elixir, lists are linked data structures. This means that each list element contains a value and an implicit reference to the next element. A list ends by linking to an empty list, marking the end of the list. This mechanism is useful to avoid endless loops, as one can check if the last element is an empty list and stop the recursive iteration.

Pattern matching with lists works similarly to pattern matching with tuples. Example:

iex> [first | rest] = [10, 20, 30, 40]

In this expression, first corresponds to the first element of the list and rest corresponds to the rest of the list. Let us check the values:

iex> first
10

iex> rest
[20, 30, 40]

This pattern matching is useful when you want to operate on the first element of a list and then process the rest of the list. But what happens if the pattern does not match the expected list? Let's look at an example:

iex> [first | rest] = []
** (MatchError) no match of right hand side value: []

In this case, since the list is empty, it is not possible to deconstruct it into a first element and the rest of the list, and thus we get a matching error.


Maps

Maps in Elixir are data structures often used to represent structured data. Often, one is only interested in certain fields of a map. We are now looking at how we can use pattern matching with maps:

iex> %{title: title, author: author} = %{title: "Elixir in Action", author: "Saša Jurić", pages: 378}
%{title: "Elixir in Action", author: "Saša Jurić", pages: 378}

In this example, we only extract the title and author fields from the map, ignoring the pages field. We check the extracted values:

iex> title
"Elixir in Action"

iex> author
"Saša Jurić"

In pattern matching, the pattern on the left need not contain all the keys in the map on the right. We can only extract the fields of interest:

iex> %{pages: pages} = %{title: "Elixir in Action", author: "Saša Jurić", pages: 378}

iex> pages
378

In this case, we only extracted the pages field, ignoring the other fields in the map.


Structs

Structs in Elixir are extensions of maps that provide a defined and secure way of working with structured data. Structs are typed and defined using modules. Let us first define a structs:

defmodule Car do
  defstruct brand: nil, model: nil, year: nil
end

We can use the same %{} syntax as we used with maps to extract fields:

iex> car = %Car{brand: "Fiat", model: "Topolino", year: 2024}
%Car{brand: "Fiat", model: "Topolino", year: 2024}

iex> %Car{brand: brand, model: model} = car
%Car{brand: "Fiat", model: "Topolino", year: 2024}

In this example, we have extracted the brand and model fields from the struct car. We verify the values:

iex> brand
"Fiat"

iex> model
"Topolino"

 

Conclusion

In this second part of our article on Elixir, we delved into some fundamental aspects of the language. We examined the various data types and how to handle them, the use of variables and modules, and the different types of functions. In addition, we explored advanced pattern matching on complex structures such as tuples, lists, maps and structs.

If you have an Elixir project in mind, do not hesitate to contact DevInterface. We are always ready to help realise innovative solutions tailored to your needs.

 

Sources:

Jurić S. (2019), Elixir in action. Manning Publications Co. 

Almeida U. (2018), Learn Functional Programming with Elixir. The Pragmatic Programmers, LLC.