Elixir est un langage de programmation fonctionnel, concurrent qui se repose sur la machine virtuelle Erlang (BEAM). Elixir est construit par dessus Erlang et partage les mêmes niveaux d'abstractions pour construire une application distribuée et résistante aux erreurs. Même si le langage est relativement récent il est déjà utilisé par des entreprises comme Pinterest, Discord, etc...
Erlang et OTP
Avant de parler du langage Elixir il est important de faire un point sur la technologie qui lui sert de base : Erlang. Je vous invite d'ailleurs à lire la définition Wikipedia qui décrit bien mieux que moi cette technologie et ses spécificités.
En lisant cette définition on peut se demander comment une telle technologie, pensée pour la téléphonie, peut s'adapter au cadre d'une application web mais au final les problématiques sont "relativement" similaires :
- Lors d'une montée en charge il faut être capable de distribuer une partie de notre application pour répondre aux besoins sans affecter le reste de l'application.
- Une requête qui crash ou un process qui échoue (échec d'un traitement d'image, d'un envoie d'email...) ne doit pas affecter toute l'application.
- Lors d'une mise à jour, on souhaite mettre à jour l'application sans couper la connexion de tous nos utilisateurs (encore plus critique dans le cas des websockets).
- Dans le cadre d'une application de petite taille, on souhaite partager le même code pour le serveur HTTP et le serveur de WebSocket (et ne pas avoir 2 codes / 2 langages à maintenir)
Erlang apporte une solution à toutes ces problématiques gràce à la structure OTP (Open Telecom Platform) qui consiste à découper notre application sous forme de processus ultra légers qui vont être capable de communiquer ensemble à travers un système de message interne à la machine virtuelle. Les processus seront accompagnés d'un superviseur capable d'agir en cas d'erreur (en redémarrant le process ou en faisant remonter le crash au superviseur parent). Cette structure donne d'ailleurs lieu à une philosophie propre à Erlang :
Let it crash
Si notre process rencontre une erreur qui n'est pas gérée, le process crash et un nouveau processus est démarré pour le remplacer. Ce processus redémarre dans un état "stable" et l'application ne se trouve pas affecté.
Attention cependant, ce système ne rendra pas votre application "fault tolerant" par magie. Si un processus crash en boucle le problème sera alors remonté et pourra affecter l'ensemble de votre application. Si la base de données est inaccessible par exemple, le processus n'arrivera pas à se reconnecter lors de son démarrage et au bout d'un certain nombre de redémarrage, fera planter le superviseur parent.
Enfin, Erlang permet de gérer nativement la distribution de son application en offrant la possibilité à l'application de communiquer avec des processus se trouvant sur un autre noeud sur le réseau.
Elixir, le langage
Erlang est un langage un peu trop "simple" qui entraine beaucoup de boilerplate et de répétition ce qui peut le rendre frustrant par moment. Elixir permet une meilleur organisation du code et offre une série de module afin de travailler plus simplement avec les outils fournis par erlang.
Les types
Le langage présente un certain nombre de types de variables.
nil # Null
1 # Entier
1.0 # Float
true # Booleen
"Salut" # Chaine (<<83, 97, 108, 117, 116>>)
'Salut' # Liste de caractères [83, 97, 108, 117, 116]
:atom # Atom
# Fonction anonyme
fn a -> a * 2 end
fn a ->
a * 2
end
[1, 2, "a"] # List
{1, 2, "a"} # Tuple
# Keyword lists
[{:a, 1}, {:b, 2}]
[a: 1, b: 2]
[
where: "...",
where: "..."
]
# Map
%{:a => 1, :b => 2, "clef" => 3}
%{a: 1, b: 2, "clef" => 3}
On remarque surtout la présence de structure similaire comme par exemple les List et les Tuple. Même si au premier abord, ces 2 types permettent de représenter les mêmes données, le stockage en mémoire est complètement différent et un choix peut s'avérer plus performant qu'un autre suivant les cas. Contrairement à certains langages on sera beaucoup plus attentifs aux performances lors de la sélection d'un type de variable.
Les chaînes de caractères peuvent aussi être représentées de 2 façons, sous forme de chaine binaire ou sous forme de liste de caractère. Les listes de caractères sont surtout là afin d'assurer la compatibilité avec d'anciennes librairies Erlang mais sont au final assez peu utilisé dans le code Elixir.
Le pattern matching
Le système de pattern matching permet de définir une fonction plusieurs fois avec des signatures différentes. Ce système est indispensable pour la récursivité, mais permet aussi de simplifier l'organisation du code.
@spec is_prime?(integer()) :: boolean()
def is_prime?(number) do
is_prime?(number, number - 1)
end
def is_prime?(number, 2), do: rem(number, 2) != 0
def is_prime?(number, divider) do
if rem(number, divider) == 0 do
false
else
is_prime?(number, divider - 1)
end
end
L'opérateur pipe
Combiner des fonctions peut gêner la lisibilité à cause du sens de lecture.
fonction4(fonction3(fonction2(fonction1(variable)), [3, 4]) + 4, "demo")
La première fonction qui est éxécutée se trouve au milieu de l'expression et le sens de lecture se fait de la droite vers la gauche ce qui est peu naturel. L'opérateur pipe permet d'écrir le même code de la manière suivante.
variable
|> fonction1()
|> fonction2()
|> fonction3([3, 4])
|> Kernel.+(4)
|> fonction4("demo")
Cet opérateur permet de refléter le principe de la programmation fonctionnel où les fonctions sont considérées comme des transformations qui sont ensuite combinées ensemble pour créer un système plus complet. Le résultat de l'opération précédente est passé à l'opération suivante.
Le meta programming
Elixir permet de modifier le langage en introduisant de nouveaux mots clefs qui seront transformés à la compilation. C'est un système qui est utilisé en interne pour les conditions par exemples :
variable = if condition do
"Condition a marché"
else
"Condition n'a pas marché :("
end
Mais qui peut aussi être utilisé pour rajouter de nouveaux verbes. Par exemple le router Plug utilise des macro pour définir des routes plus facilement.
defmodule MonSuperRouter do
use Plug.Router
plug :match
plug :dispatch
get "/hello" do
send_resp(conn, 200, "world")
end
forward "/users", to: UsersRouter
match _ do
send_resp(conn, 404, "oops")
end
end
En revanche, il ne faudra pas trop en abuser au risque de rendre le code difficile à comprendre car il n'est pas forcément évident de comprendre ce qui se cache derrière une macro.
Module
Nos fonctions seront organisées dans des modules qui servent de "namespace".
defmodule Number do
@moduledoc """
Permet de faire des tests sur les nombres
"""
@doc """
Vérifie si un nombre est pair
## Examples
iex> Number.is_pair?(14)
true
iex> Number.is_pair?(3)
false
"""
@spec is_pair?(integer()) :: boolean()
def is_pair?(number) do
rem(number, 2) == 0
end
@doc """
Vérifie si un nombre est premier
## Examples
iex> Number.is_prime?(29)
true
iex> Number.is_prime?(14)
false
"""
@spec is_prime?(integer()) :: boolean()
def is_prime?(number) do
is_prime?(number, number - 1)
end
defp is_prime?(number, 2), do: rem(number, 2) != 0
defp is_prime?(number, divider) do
if rem(number, divider) == 0 do
false
else
is_prime?(number, divider - 1)
end
end
@doc """
Trouve le premier nombre premier d'une liste
## Examples
iex> Number.first_prime([2, 14, 29, 12, 3])
29
iex> Number.first_prime([10, 6, 9])
nil
"""
@spec first_prime(list(integer())) :: integer()
def first_prime(numbers) do
numbers
|> Enum.filter(fn (number) -> is_prime?(number) end)
|> List.first()
end
end
Superviseur & Process
Créer un superviseur est extrèmement simple gràce au module Application
defmodule Counter.Application do
use Application
def start(_type, _args) do
import Supervisor.Spec, warn: false
# Définit les enfants à superviser
children = [
worker(Counter.Worker, []),
]
# Définit les options de notre superviseur et lui donne un nom
opts = [strategy: :one_for_one, name: Counter.Supervisor]
Supervisor.start_link(children, opts)
end
end
Les processus enfants peuvent être de différents types et se basent sur GenServer
defmodule Counter.Worker do
use GenServer
def start_link() do
state = %{
number: 0
}
GenServer.start_link(__MODULE__, state, [name: :counter])
end
def get_number() do
GenServer.call(:counter, :get)
end
def increment() do
GenServer.cast(:counter, :increment)
end
def handle_call(:get, _from, state) do
{:reply, state.number, state}
end
def handle_cast(:increment, state) do
new_state = Map.put(state, :number, state.number + 1)
{:noreply, new_state}
end
end
La fonction start_link
permet de démarrer le processus et sera automatiquement lancée par le superviseur. Elle permet de définr l'état de départ du processus qui sera ensuite gardé en mémoire.
Les fonctions handle_call
et handle_cast
permettent au processus de répondre aux messages qui lui seront envoyés. En plus de la réponse, ces fonctions peuvent renvoyer un nouvel état afin de garder en mémoire l'état du processus. Ce système d'état permet de contrebalancer la nature immutable de la programmation fonctionnelle et de faire évoluer le système.
Mix
mix
est un outil qui permet de gérer un projet simplement. Par exemple, créer un projet peut se faire très simplement.
# Créer une nouvelle application
mix new app
# Créer une nouvelle application avec un superviseur
mix new app --sup
Mais cette commande nous servira aussi à compiler notre application ou lancer les tests
mix compile
mix test
Tests unitaires
Les projets générés intègrent les tests unitaires de base et utilisent le système de macro pour une écriture simplifiée.
defmodule PairTest do
use ExUnit.Case
doctest Number
test "the truth" do
assert 1 + 1 == 2
end
end
Les tests peuvent être aussi écrit lorsque l'on documente une fonction.
@doc """
Vérifie si un nombre est pair
## Examples
iex> Number.is_pair?(14)
true
iex> Number.is_pair?(3)
false
"""
@spec is_pair?(integer()) :: boolean()
def is_pair?(number) do
rem(number, 2) == 0
end
Hex, un gestionnaire de paquet
mix permet aussi d'accéder au gestionnaire de paquet hex.pm
afin d'intégrer plus facilement des librairies tiers au niveau de son projet.
mix deps.get
On appréciera notamment la génération automatique de la documentation qui permet de centraliser toutes les informations et présenter les choses de manière uniforme.
Phoenix, un framework web
Enfin, si vous souhaitez utiliser elixir pour créer une application web vous pouvez jeter un oeil au framework Phoenix qui vous offre tous les outils dont vous avez besoins pour vous lancer.