Skip to content

Elixir, functional programming for everyone

24 mayo, 2021

Functional programming is all the rage. More and more languages ​​are adopting this paradigm, but more importantly, developers are adopting more and more languages ​​of this type. Scala, F #, Clojure and other old rockers like Erlang or Haskell are beginning to be on the lips of many of the programmers in the sector.

One of the newest in town is Elixir, a functional, concurrent language designed to make maintainable and scalable applications.

Elixir

Elixir runs on Erlang’s virtual machine (BEAM). This makes the language, despite being very young, robust and with a lot of functionality

Elixir is a language created in 2011 by José Valim, and although the syntax is new, the language runs on the Erlang virtual machine (BEAM). This makes the language, despite being very young, robust and with a lot of functionality. In addition, as the language is compiled to Erlang bytecode, functions of that language can be used, without any type of penalty.

Elixir, like Erlang, is a dynamic language, so we will receive the bugs at runtime. Elixir’s typing, although dynamic, is strong, so things that are allowed in languages ​​with a weaker type, such as JavaScript, are not allowed. For example, the operation 1 + "1" it is perfectly valid in JavaScript, but causes a runtime error in Elixir.

Installation and configuration

Installing Elixir is very simple and you just have to follow the steps indicated on its website. We can install it on Linux, Windows or Mac without too much trouble.

Once we have done it, there is no additional configuration to do. Writing iex On the command line we will access the REPL (Read-Eval-Print-Loop) of Elixir, which will allow us to execute Elixir instructions. In fact, we could write an entire application from this console.

Elixir logo

Organization and syntax

The syntax is based on Ruby syntax, so those with some experience in Ruby will find the way to write code in Elixir familiar. Let’s see an example:


defmodule GenbetaDev.Syntax.Example1 do

  def print(option) do
    option
    |> get_message
    |> IO.puts
  end

  defp get_message(opt) do
    cond  do
      opt == 1 -> "Hello"
      opt == 2 -> "Hello, World"
      true -> "Hello planet Earth"
    end
  end
end

The functions in Elixir are distinguished by their name and number of parameters (arity)

All the code in Elixir, is organized in modules with defmodule and within these modules the functions that will make up our application are declared. Functions can be public def and accessible from outside the module, or private defp and only accessible if they are called from within the same module. The functions in Elixir are distinguished by their name and number of parameters (arity). The function get_message from the example, it is described by the Elixir compiler as get_message/1. If we had another function with the same name, but receiving two parameters, it would be get_message/2.

Although in Elixir there are no namespacesBy convention, modules are often named in such a way that they are organized. For example in our example the module is called GenbetaDev.Syntax.Example1, which could be something like the name of the project or application, then the group of modules, and finally the specific name of the module. When these names are too long, we can use the operator alias, and thus write only the last part of the name (in this case Example1).

The magic of pattern matching

One of the most interesting features of Elixir is the pattern matching. Although most functional languages ​​have this type of mechanism, in Elixir it is used in a very elegant way. The pattern matching or concordance of patterns, is nothing more than looking for a similarity with a pattern, to perform a specific action. In Elixir, this pattern matching is continuously present, so for example we can see things like this (from iex in this case):


iex(1)> x = 1
1
iex(2)> 1 = x
1
iex(3)> 2 = x
** (MatchError) no match of right hand side value: 1

We first assign the value 1 to a variable. Then we use the operator = to check for a match. In the first case, there is, since x = 1, in the second no, and Elixir shows us an error.

If we complicate the example, we can see that pattern matching also works with tuples:


iex(4)> {a, b, c} = {1, :error, "not found"}
{1, :error, "not found"}
iex(5)> {2, :error, "null exception"} = {a, b, c}
** (MatchError) no match of right hand side value: {1, :error, "not found"}
    
iex(5)> {1, :error, "not found"} = {a, b, c}     
{1, :error, "not found"}
iex(6)> 1 = a
1    
iex(7)> :error = b
:error
iex(8)> "not found" = c
"not found"
iex(9)> :ok = b
** (MatchError) no match of right hand side value: :error

And that also works with lists:


iex(10)> [a, b, c] = [1, 2, 3]
[1, 2, 3]
iex(11)> a
1
iex(12)> 9 = a
** (MatchError) no match of right hand side value: 1
    
iex(12)> [3, 4, 5] = [a, b, c]
** (MatchError) no match of right hand side value: [1, 2, 3]
    
iex(12)> [1, 2, 3] = [a, b, c]
[1, 2, 3]

As I have already said, pattern matching is usually a feature of functional languages, but in Elixir it is included in such a way that we are almost invited to use it. In fact, when it comes to calling functions it is terribly useful. For example, this would be the typical FizzBuzz code.


defmodule GenbetaDev.Examples.FizzBuzz do

  def start(first, last) do
    first..last
    |> Enum.each(fn(x) -> check(x) end)
  end

  defp check(number) when rem(number, 15) == 0, do: IO.puts("FizzBuzz") 
  defp check(number) when rem(number, 3) == 0, do: IO.puts("Fizz")
  defp check(number) when rem(number, 5) == 0, do: IO.puts("Buzz")
  defp check(number), do: IO.puts("#{number}")

end

If a number is divisible by 3 we write “Fizz”. If it is divisible by 5 we write “Buzz”. If it is divisible by both we write “FizzBuzz”. In any other case we write the number.

In the example we define four functions that are called the same, and that receive the same number of parameters. With the guardian clauses when We define the conditions that must be met for the pattern matching to use this function. Elixir checks from top to bottom which function to use. Therefore, the most specific case should always be at the beginning and the most general, at the end. The good thing about pattern matching in functions is that it does not have to be applied with guard clauses, but we can apply it to the value of the parameter. For example with tuples:


def print_result({:ok, _}), do: IO.puts("operación completada con éxito")
def print_result({:error, message}), do: IO.puts(message)
def print_result(_), do: Io.puts("Error en el parámetro recibido")

In short, pattern matching saves us from having to write a multitude of conditional statements that would make the code more complicated to follow. In fact, the general recommendation is to use pattern matching whenever possible. And in fact it is so easy to do it, that we do not usually find impediments.

Duties as first-class citizens

As a good functional language, Elixir uses functions as first-class citizens. This means that functions can be passed as parameters, assigned to variables, or received as a result of a function.

We have seen before that we can define functions with def Y defp. In addition we can also define anonymous functions:


iex(18)> myfun = fn(x) -> x * 2 end
#Function    
iex(19)> myfun.(2)  
4
iex(23)> myfun3 = fn(x) -> myfun.(x) + 1 end
#Function
iex(24)> myfun3.(2)
5

In any case, we can assign functions to variables, or even pass them as a parameter of another function. For example:


defmodule GenbetaDev.Examples.FirstClassFunctions do
  def executor(func, n) do
    func.(n)
  end
end

The example function receives another function as the first parameter and executes it by passing it the second parameter n. In elixir to execute a function contained in a variable, you always have to do it by adding .(). Two examples:


iex(3)> alias GenbetaDev.Examples.FirstClassFunctions
alias GenbetaDev.Examples.FirstClassFunctions
GenbetaDev.Examples.FirstClassFunctions

iex(4)> FirstClassFunctions.executor(fn(x) -> x * 3 end , 4)
FirstClassFunctions.executor(fn(x) -> x * 3 end , 4)
12

iex(5)> FirstClassFunctions.executor(fn({x, y}) -> x  y  end , {"hola", " GenbetaDev"})
FirstClassFunctions.executor(fn({x, y}) -> x  y  end , {"Hola", " GenbetaDev"})
"Hola GenbetaDev" 

OTP the killer feature by Elixir

The Elixir syntax is quite affordable for all types of programmers and the pattern matching is very successful and easy to use, but is this enough to adopt Elixir? Probably not, but if we think about the possibilities we have with OTP, things change.

OTP (Open Telecom Platform) is a set of Erlang libraries and functionalities that allow you to work easily and affordably with concurrent programming. And how could it be otherwise Elixir drinks from it to offer us the possibility of using it.

actors

The concurrent processes are totally independent and do not share any type of information

It must be taken into account that Erlang and OTP were initially intended for use in telephone switchboards, so when designing OTP they were based on an actor model. This means that the concurrent processes are totally independent and do not share any type of information.. When one process wants to communicate with another, it can only do so through message passing (through a mailbox), which the target process will process when it sees fit or is possible.

This makes the processes (actors) that we start with OTP very light and hardly consume resources, unlike with other types of languages, in which the contexts of the processes are heavy and it is more difficult to manage them. The result is that we can start hundreds of thousands of processes with little penalty to the system. This gives us truly incredible power.

Obviously, with this level of concurrency, we need tools that allow us to manage the execution (and failure) of the processes that are launched. And for this we have supervisors.

Using supervisors

A supervisor is in charge of managing as many child processes as necessary. The supervisors are configured with a specific strategy, which they will follow in case any of the child processes have problems. For example, the strategy may be to restart a process when it fails, restart all child processes when one fails, or not restart any process when it fails. And all this is handled by the supervisors, efficiently, so we will have to worry much more about it.

If we launch a process, but it suffers an exception, its supervisor is in charge of managing it and, for example, it will start a new process instantly. It is also possible that the processes store a state that can even be preserved in case of failure.

Let it crash

As Elixir processes are so …