À titre d'exemple nous allons mettre en place une todo-list (liste de tâches). Une todo-list se présente comme une liste de tâches à effectuer ("acheter des tomates", "prendre une douche", "manger"...), à laquelle on peut ajouter des tâches (quand le devoir nous appelle) ou en retirer (quand on les a effectuées).
Notre todo-list a la particularité d'être
multi-utilisateurs, c'est-à-dire qu'elle est accessible par plusieurs personnes 'en même temps' : tout le monde peut y ajouter ou en enlever des tâches, ou consulter la liste.
On peut aussi imaginer des modèles plus précis (par exemple dans une école : les professeurs ne font qu'ajouter des tâches, et vous, vous devez les retirer

), mais celui-là est suffisamment simple pour mettre en oeuvre la plupart des
outils de base du langage, tout en restant compréhensible.
Voici le code :
Code : Erlang 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | -module(todo_list).
-export([start/0, loop/1]).
loop(Liste) ->
receive
{add, X} -> loop([X|Liste]);
{del, X} -> loop([Y || Y <- Liste, Y =/= X]);
{From, show} -> From ! Liste,
loop(Liste);
close -> io:format("Fin de la connexion~n")
end.
start() ->
io:format("Création d'un processus~n"),
spawn(todo_list, loop, [[]]).
|
Les deux premières lignes servent en fait à
déclarer notre programme. Par cette dénomination pour le moins surprenante, je veux signifier que ces deux lignes vont nous permettre de réutiliser le code qui va être écrit ensuite.
Code : Erlang1
2 | -module(todo_list).
-export([start/0, loop/1]).
|
Ces deux directives ne sont pas intéressantes pour une première approche : elles s'occupent de la portée des variables et de l'interaction de ce fichier avec le reste du code, un peu comme l'inclusion de
.h en C, et les déclarations de portée (publique / privée).
Comme cela a été expliqué dans la première partie, de multiples schémas de communication sont possibles. Ici, nous allons mettre en oeuvre une architecture
client-serveur (simplifiée). Plus exactement, ce code ne contient que le comportement du serveur (fonction
loop), c'est-à-dire la partie qui gère l'accès et la modification par tous les utilisateurs (les "clients") de la liste ; les clients sont très simples, et on peut passer directement par la console Erlang pour cela.
Le serveur fonctionne d'une manière assez spécifique aux programmes Erlang, que l'on peut décrire de la manière suivante :
on donne la liste à un "employé", on lui dit "garde-là tant que tu ne reçois pas de message". S'il reçoit un message, différents cas se présentent, selon le contenu du message ; on gèrera ici trois types de messages : "ajouter la tâche machin", "retirer la tâche bidule", "montrer la liste à la personne truc". Il agit en conséquence, et la partie spécifique se déroule à ce moment-là : au lieu de
modifier la liste des tâches, il donne une
autre liste à un nouvel employé, qui est alors chargé de répéter le processus. Par exemple, si le message était "ajoute la tâche 'manger'", il va donner à un autre employé sa liste, ainsi que le message "manger" (ce qui constitue donc une nouvelle liste plus grande), et c'est ce nouvel employé qui s'occupera des messages suivants.
C'est une mise en oeuvre particulière de la
récursivité, un concept décrit dans un des
tutos du SDZ.
Voyons maintenant le code. La structure
receive .. end permet d'examiner les messages, et d'agir en fonction de leur contenu. Si on prévoit de recevoir deux types de messages différents, le code aura cette tête-là (où
expression désigne un bout de code qui renvoie une valeur) :
Code : Erlang1
2
3
4 | receive
premier_type -> expression;
deuxieme_type -> expression
end
|
Code : Erlang1
2
3
4
5
6
7
8 | loop(Liste) ->
receive
{add, X} -> loop([X|Liste]);
{del, X} -> loop([Y || Y <- Liste, Y =/= X]);
{From, show} -> From ! Liste,
loop(Liste);
close -> io:format("Fin de la connexion~n")
end.
|
On peut remarquer que le point-virgule (;) ne sert pas à séparer les instructions, mais à séparer les différents cas possibles. Pour exécuter deux instructions à la suite, on utilise une simple virgule.
Ici, on reçoit quatre types de messages :
- l'ajout d'un élément à une liste ;
- la suppression d'un élément de la liste ;
- la demande d'affichage de la liste à quelqu'un ;
- un message de fin, en cas de fermeture de l'application.
Le message de fermeture est "simple" : c'est le message
close : quand on le reçoit, on envoie un message de fermeture, et on s'arrête. Les autres messages sont un peu plus délicats parce qu'ils contiennent de l'information : quand on envoie le message "ajoute à la liste", il faut préciser l'élément à ajouter : il est contenu dans le message. De même, le message "montre la liste à machin" doit contenir l'adresse de machin, pour que le serveur puisse lui envoyer la liste. Pour faire cela, on utilise des messages en plusieurs parties :
{..., ...} est un message en deux parties. Certaines parties sont fixes (par exemple
add et
del) : on appelle ça des
atomes, et on peut voir cela un peu comme des constantes définies par le programmeur. D'autres parties sont variables : le
X dans les deux premiers messages est une variable qui contient la valeur donnée (et dépend donc du message reçu). Les parties fixes sont en minuscules, et les parties variables commencent par une majuscule.
Le message d'ajout fonctionne simplement : si l'on reçoit
{add, X} (on sait que c'est un message d'ajout grâce à la présence de l'atome
add), on rappelle
loop avec la nouvelle valeur
[X|Liste], c'est-à-dire une liste qui contient tous les éléments de la liste initiale, plus le contenu de la variable
X. (c'est là qu'on doit imaginer que l'on donne cette nouvelle liste à un nouvel employé).
La nouvelle liste donnée en cas de message de suppression est un peu particulière : la syntaxe
[Y || Y <- Liste, truc(Y)] signifie "tous les éléments Y de la liste, qui vérifient 'truc'". Ici, on sélectionne tous les éléments de la liste qui sont
différents de
X : à la fin, on a donc la liste, sauf la valeur de
X, qui a donc bien été supprimée. C'est ce qu'on appelle une
compréhension de liste (expression maladroite venant de l'anglais "list comprehension").
Enfin, le message "montrer la liste à machin" met en oeuvre une deuxième structure essentielle à la communication inter-processus en Erlang, l'envoi de messages :
!. La syntaxe est
Pid ! Msg, et cela envoie le message
Msg au processus dont l'adresse est
Pid. Ici, cette adresse a été donnée dans le message, c'est la valeur
From (vous pouvez remarquer que contrairement aux deux premiers messages, la partie variable a été placée en premier : c'est la convention quand on envoie son adresse dans un message). La valeur que l'on envoie est
Liste : on envoie bien le contenu de la liste de tâches au processus dont l'adresse est
From. Ensuite (après la virgule) on rappelle
loop(Liste) : le serveur continue à tourner, avec la même liste.
Voici enfin la dernière fonction du programme, qui joue un peu le rôle du "main" en C : c'est la fonction de départ, qui est appelée au lancement du programme.
Code : Erlang1
2
3 | start() ->
io:format("Création d'un processus~n"),
spawn(todo_list, loop, [[]]).
|
La fonction
start contient deux instructions séparées par une virgule (Erlang utilise le point-virgule pour dénoter un autre type de séparation, c'est donc la virgule que l'on utilise pour séparer deux instructions ; les fonctions sont séparées par des points). La première instruction,
io:format, affiche du texte sur la sortie standard.
La deuxième instruction est plus intéressante : ll s'agit de la dernière des trois structures principales de gestion de la concurrence en Erlang : c'est la fonction
spawn, qui lance un nouveau processus, et renvoie un identifiant le concernant.
Les arguments contiennent le module à utiliser (ici
todo_list), le nom de la fonction à appeler (
loop), et enfin une liste d'arguments à donner à cette fonction : avec
[[]], on donne un seul argument qui est
[], la liste vide : au départ, notre todo-list sera vide.
Et le client ?
Le client ne présente que peu d'intérêt : il suffit d'envoyer au serveur les bons messages, et cela marche tout seul.
Pour une mise en oeuvre rapide de cette todo-list, on peut utiliser la console Erlang. C'est un environnement interactif (un peu comme la ligne de commande sous GNU/Linux) qui permet de manipuler des modules Erlang de manière simple, pour faire des tests par exemple.
Le résultat se présente ainsi : les lignes qui commencent par un nombre suivi de
> sont les lignes de code que l'utilisateur a entrées. Les lignes qui les suivent sont les résultats renvoyés par la console. Ici, l'utilisateur manipule notre module
todo_list, en envoyant une tâche au serveur, avant de récupérer la liste des tâches. Les phrases après
%% sont des commentaires : elle servent d'explications mais ne sont pas lues par l'interpréteur.
Code : Erlang 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | 1> c(todo_list). %% cette commande sert à compiler le module
{ok,todo_list}
2> Serv = todo_list:start(). %% on initialise la variable Serv avec le pid (l'adresse)
Creation d'un processus %% du processus créé dans start (le serveur)
<0.38.0>
3> Serv ! {add, "faire mes devoirs"}. %% on ajoute un élément à notre todo-list
{add,"faire mes devoirs"}
4> Serv ! {self(), show}. %% self() permet d'obtenir le pid du processus courant,
{show, <0.31.0>} %% nécessaire pour que le serveur puisse répondre
5> receive Liste -> Liste end. %% reçoit la réponse du serveur et on l'affiche
["faire mes devoirs"]
6> Serv ! close. %% on ferme la connexion
Fin de la connexion
close
7>
|