Un jour, Max a eu envie de créer un logiciel de chat dédié au monde du porno. Quand il a ce genre d’idée à la con qui va prendre 10 ans de développement, il fait ce qu’aucun développeur expérimenté ne ferait : il se lance directement dans un truc très compliqué qu’il ne comprends pas, il le fait marcher au hasard en 10 jours, et après il se fout de la gueule des gens qui lui ont dit que ça ne marcherait jamais. Moi le premier.

Sam, si c’est idiot et que ça marche. Ce n’est pas idiot.

La capacité à développer au radar mérite un article à lui tout seul, mais en l’occurence cet article est là pour parler d’Erlang, le langage qui fait tourner eJabberd, le serveur de chat que Max avait installé.

Ce serveur a été bidouillé, transconfiguré, des plugins ont été rajoutés à chaud, enlevés, recompilés, avec erreur, sans erreur. Peu importe, le serveur n’a jamais cessé de fonctionner malgré tous les mauvais traitements qu’on lui a fait subir.

Devant cette étonnante robustesse, nous avons regardé le code source : un ensemble de symboles absolument incomprehensibles qu’une recherche sur le net nous a permis d’identifier sous le nom d’Erlang, pour Ericsson Language.

Ce langage de programmation a été en effet crée par la firme de téléphonie il y a 20 ans afin de gérer ses infrastructures avec le minimum de défaillances. Et de notre expérience, ça marche plutôt bien, mais il faut également pouvoir se taper la syntaxe qui tient plus des maths que de la prog.

Exemple avec la fonction factorielle en Python(1):

def fact ( n ) : n = abs ( n ) or 1 for i in xrange ( n - 1 , 1 , - 1 ) : n = i * n return n def fact(n): n = abs(n) or 1 for i in xrange(n - 1, 1, -1): n = i * n return n

Le même chose en Erlang:

fact ( N ) -> fact ( N , 1 ) . fact ( 0 , Acc ) -> Acc ; fact ( N , Acc ) when N > 0 -> fact ( N - 1 , N * Acc ) . fact(N) -> fact(N,1). fact(0,Acc) -> Acc; fact(N,Acc) when N > 0 -> fact(N-1,N*Acc).

Notez que c’est une implémentation à récursion terminale, et comme Erlang n’accumule pas les stack frames en cas de TCO, elle est équivalente en terme de performance à la version itérative de Python. On pourrait faire plus simple, mais ça consommerait plus de ressources. Et si vous n’avez rien pigé à ce paragraphe, ne vous inquiétez pas, je ne l’aurais pas compris encore ce matin.

Comme on pense remettre le couvert avec un projet de chat plutôt maousse, et que le stateful temps réel est le point fort du langage, je me suis dis que j’allais me bouffer un petit tuto. Je vous fais le tour du proprio.

Il n’y a pas de boucle

Pas de for. Pas de while. Pas de foreach. Nada. Tout se fait par récursion, et si vous n’avez pas fait beaucoup de programmation fonctionnelle dans votre vie, c’est un vrai chamboulement dans la manière de penser un programme.

Heureusement, comme Python, Erlang vient avec un tas d’outils qui permettent d’effectuer des traitements itératifs sans se noyer(noyer(noyer())) :

listes en intention;

unpacking;

fonctions anonymes;

any(), all(), takewhile(), dropwhile(), zip() , etc. Tout ce qu’on trouve dans Python itertools et bien plus.

Les variables sont psychorigides

Comme en Python, les variables sont types dynamiquement (pas besoin de déclarer le type) et fortement (pas de transtypage implicite). Python possède également le concept de variables non mutables, par exemple, on ne peut modifier un entier. Mais Erlang va plus loin, et interdit la modification d’une variable dans le même scope :

8> A = 1. 1

9> A = 2. ** exception error: no match of right hand side value 2

En clair, quand vous faites un assignement en Erlang, il est là pour rester. C’est un choix à la fois technique et philosophique, qui permet au langage se prémunir encore plus des effets de bord, et d’encourage l’usage des messages.

Comme à la poste

Car Erlang fonctionne ainsi : vous avez une machine virtuelle qui fait tourner du bytecode, comme en Python. Mais contrairement à Python, on peut diviser le travail en des centaines d’actors, un concept qui est un mélange entre un worker et un objet.

Comme un objet car il encapsule son propre état mais peut le partager avec d’autres, contrairement à un worker, et qu’il tourne avec la même machine virtuelle que tous les autres. Mais comme un worker car chaque actor est un process séparé, qui vit, plante, et travaille dans son coin. En total concurrence avec les autres acteurs.

Par exemple si vous avez un jeu video en ligne, chaque joueur peut être un acteur dans le programme.

Pas de problème de GIL, de thread, de synchronize, ou quoi que ce soit. Chacun chez soi, et quand on veut partager une information ou donner un ordre, on envoit un message, comme une lettre, qui peut arriver ou non, être traité ou non, et obtenir une réponse… ou non.

On ne part pas du principe que tout marche, on essaye juste de faire marcher le plus de choses possible, le mieux possible. En effet Erlang possède un mécanisme de gestion des erreurs perfectionné, et faire crasher un process fait partie intégrale du workflow.

C’est assez dur à appréhender au début. Comme quand on vient de Java (LBYL) et qu’on apprend qu’en Python, on utilise try/catch pour la gestion de flux (EAFP). Encore une fois, Erlang va plus loin, et utilise le crash comme gestion de flux. En cas de plantage, le process est simplement collecté, et relancé. Cette opération est très légère en Erlang, tout comme le passage de messages ou la concurrence entre les actors.

Cela rend non seulement Erlang résitant aux pannes, mais cela simplifie aussi grandement la gestion des erreurs. On ne se demande plus si les choses vont marcher, on se demande uniquement ce qu’on va faire quand ça marche.

Les aspects zarbs

Le pattern matching. C’est une forme de filtre qui utilise l’unpacking. Difficile à résumer en deux mots, d’autant qu’il s’utilise un peu partout. Il va me falloir du temps à me forcer à raisonner avec.

Le return implicite. Si vous avez programmé en coffee script, vous vous sentirez comme à la maison. En plus la syntaxe des fonctions est similaires.

Les atoms. Le concept le plus proche serait un mix entre les symbols en Ruby et les enums en C. Rajoutez à cela qu’il n’y a pas de valeur null comme None.

Les opérateurs de comparaisons et les conditions sont franchement tordues. Et il y a la notion de “guards“, une forme de check qui remplace la vérification de type par une vérification de condition, mais qui n’accepte qu’un subset limité d’appel de fonctions dans le corps de l’instruction. Après quelques recherche, la majorité des gens se limitent à utiliser l’instruction “case” au maximum, qui reste ce qu’il y a de plus clair.

or, and, orelse, andelse. La première lettre des variables obligatoirement en majuscule. Pas de clause “else”. Et des instructions qui terminent non par “;”, mais par “.”. “%” comme début de commentaire. C’est louche.

Par exemple, en Python(2) :

def help_me ( animal , _says = "fgdadfgna" ) : if animal == "cat" : _says = "meow" if animal == "beef" : _says = "mooo" if animal in ( "dog" , "tree" ) : _says = "bark" return ( animal , "says " + _says + "!" ) def help_me(animal, _says="fgdadfgna"): if animal == "cat": _says = "meow" if animal == "beef": _says = "mooo" if animal in ("dog", "tree"): _says = "bark" return (animal, "says " + _says + "!")

En Erlang :

help_me ( Animal ) -> Talk = if Animal == cat -> "meow" ; Animal == beef -> "mooo" ; % ...dur de faire la différence entre ";" Animal == dog -> "bark" ; Animal == tree -> "bark" ; true -> "fgdadfgna" end , % ... et "," ... { Animal , "says " ++ Talk ++ "!" } . % ... et "." help_me(Animal) -> Talk = if Animal == cat -> "meow"; Animal == beef -> "mooo"; % ...dur de faire la différence entre ";" Animal == dog -> "bark"; Animal == tree -> "bark"; true -> "fgdadfgna" end, % ... et "," ... {Animal, "says " ++ Talk ++ "!"}. % ... et "."

En conclusion

La syntaxe alambiquée, la gestion des strings misérables, l’obligation de compiler et l’absence de certains outils de confort comme les paramètres par défaut des fonctions rendent Erlang a priori peu attratif pour un codeur Python.

Pourtant les deux langages ne sont pas si différents. On retrouve les listes, les tuples et les modules. On sent qu’Erlang est plus proche de sa logique métier, et que Python est plus abstrait, et plus généraliste. Mais un développeur à l’aise avec l’un saura retrouver ses billes avec l’autre, d’autant qu’un shell interactif est présent dans chacun des cas.

Cependant se pencher sur Erlang est vraiment inspirant. On y découvre une approche nouvelle de problèmes qu’on connaissait déjà, et des paradigmes différents qui bénéficieront forcément aux prochaines sessions de dev sous M150. Un peu comme se lancer dans Lisp, seulement avec l’impression que ça peut servir à quelque chose.

Mais le plus important reste que Erlang et Python sont vraiment deux langages complémentaires. Python est un maître dans l’analyse de données, le trairement de chaînes et le scripting tout en étant une grosse bille pour les process concurrents. Erlang n’aime pas les calculs rapides ou les jeux de chiffres fournis, par contre quand il faut gérer des milliers de petites tâches en même temps tout en se prenant des baffes dans le gueule, c’est un champion.

Bref, c’est un outil intéressant à garder sous la main. Je ne sais pas à quel point cela va être praticable de développer un gros projet avec, mais si un gros opérateur télécom nordique l’utilise depuis deux décénies, je suis assez optimiste. Et puis c’est ça ou Node.js, alors quitte à se taper un langage de merde pour traiter massivement de la concurrence, autant choisir une techno qui a déjà un serveur de jabber bien rodé.

(1) En vérité, si le module Python math n’avait pas déjà factorial() , j’écrirais plutôt la fonction ainsi :

def fact ( n , _mul = lambda x , y: x * y ) : return reduce ( _mul , xrange ( abs ( n ) , 1 , - 1 ) , 1 ) def fact(n, _mul=lambda x, y: x * y): return reduce(_mul, xrange(abs(n), 1, -1), 1)

Mais je suppose qu’un expert Erlang aurait également une version de la mort qui tue.

(2) Ok, c’est moche. On ferait plutôt ça:

SAYS = { "cat" : "meow" , "beef" : "moo" , "dog" : "bark" , "tree" : "bark" } def help_me ( animal , default = "fgdadfgna" , says = SAYS ) : return ( animal , "says %s!" % says. get ( animal , default ) ) SAYS = {"cat": "meow", "beef": "moo", "dog": "bark", "tree": "bark"} def help_me(animal, default="fgdadfgna", says=SAYS): return (animal, "says %s!" % says.get(animal, default))

Et en Erlang surement un “case”.