Passer à Elixir
(leemeichin.com)- Après avoir rejoint une entreprise dont Elixir est le langage principal côté backend, un développeur backend Ruby s’est adapté assez rapidement, même sans expérience d’Elixir/Erlang, grâce à une syntaxe influencée par Ruby
- Elixir offre, comme Ruby, un plaisir syntaxique et une affinité avec les DSL, tout en réduisant la charge de gestion de l’état grâce à un modèle fonctionnel immuable sans classes, instances ni héritage
- Alors que les DSL Ruby s’appuient sur la définition dynamique d’objets et de méthodes à l’exécution, les DSL Elixir génèrent du code d’exécution via des macros à la compilation, ce qui fait apparaître les erreurs plus tôt
- La syntaxe de pipeline, l’expression
withet les tuples Result de la forme{:ok, result}sont utiles pour séparer le flux normal du traitement des erreurs récupérables - Par rapport à l’approche Ruby reposant sur les exceptions,
nil/falseet l’état d’erreur interne des objets, les retours explicitesok/errord’Elixir semblent plus satisfaisants, et l’expérience d’écriture reste agréable après trois mois d’utilisation
Comment un développeur Ruby s’est adapté à Elixir
- Le nouveau poste utilise Elixir comme principal langage backend, alors qu’avant de commencer il n’avait jamais écrit de code Elixir ni Erlang
- Erlang ne lui était pas trop étranger grâce à une petite expérience avec Prolog, et Elixir a pu être appris rapidement grâce à une syntaxe fortement influencée par Ruby
- Il n’est pas encore expert des bonnes pratiques, de l’architecture ni des concepts Erlang de plus bas niveau, mais cela n’a pas empêché d’entrer dans Elixir
- Elixir est perçu comme un langage qui conserve le plaisir de programmer qu’offre Ruby, tout en réduisant les pièges liés à l’état fréquents dans les langages orientés objet
- Pas de classes, d’instances ni d’héritage
- Fondé sur l’immuabilité et un style fonctionnel
- Pas lié à un système de types statique
- Le code avec état mutable est beaucoup plus difficile à maintenir, et il n’est pas toujours clair si l’état est local à une fonction, à une instance ou à une classe
- En Ruby, tout est objet, chaque objet peut donc avoir son propre état, et lorsqu’on cherche à créer des DSL intuitifs, les états au niveau des fonctions, des instances et des classes peuvent facilement se mélanger
- L’eigenclass de Ruby est un concept proche d’une classe de classe, et une instance peut modifier son eigenclass de manière à affecter aussi l’état d’autres instances
DSL et différence avec les macros à la compilation
- Elixir prend aussi en charge les DSL et sa syntaxe est familière, comme Ruby, mais la différence essentielle tient au moment où le DSL est créé
- Les DSL Elixir sont des macros à la compilation qui génèrent du code d’exécution
- Les DSL Ruby reposent sur la modification du runtime pour définir dynamiquement objets et méthodes
- Si une macro Elixir est mal utilisée, l’erreur se produit dans la bibliothèque, et si le code de macro généré est incorrect, une erreur de compilation survient
- Comme ces erreurs apparaissent à la compilation, les tests peuvent se concentrer davantage sur la logique applicative réelle
Le flux créé par les pipelines et with
- L’opérateur de pipeline
|>d’Elixir est utile, mais les opérateurs de threading de Clojure ont aussi leurs avantages->en Clojure place le résultat de l’expression précédente comme premier argument de la fonction suivante, comme|>en Elixir->>en Clojure place le résultat précédent comme dernier argument de la fonction suivante- Même si cette différence semble minime, elle est utile pour écrire du code d’interopérabilité sans recourir à des fonctions anonymes
- L’expression
withd’Elixir rappelle leletde Haskell ou Lisp, tout en se comportant comme si unotherwiseétait intégré- Elle permet de diviser une fonction complexe entre le chemin nominal en haut et la gestion des erreurs en bas
- Si le pattern matching sur
{:ok, result}échoue, cela peut être traité comme une erreur récupérable, ce qui s’accorde bien avec les tuples Result
Les avantages des retours d’erreur explicites
- En Ruby, il est courant d’utiliser les exceptions pour le flux de contrôle, et de s’appuyer sur un résultat de succès,
nil,falseou un état d’erreur interne à l’objet plutôt que sur un type d’erreur explicite- Par exemple,
model.update(params)renvoiefalseen cas d’échec et stocke la raison de l’échec dansmodel.errors - Cela ramène au problème de l’état mutable
- Par exemple,
- Une approche consistant à expliciter dans les valeurs de retour
ok/errorouleft/right, comme avec les monadesResult/Either, paraît plus satisfaisante que le flux d’exceptions à la Ruby - Après trois mois d’utilisation d’Elixir, Elixir reste un langage qui prolonge le sentiment montré par Ruby : « programmer, c’est amusant »
1 commentaires
Avis Hacker News
Ces dernières semaines, je regarde presque tous les jours des vidéos YouTube sur Elixir
Il semble qu’il y ait eu une conférence Elixir il y a peu, et après en avoir regardé quelques-unes, YouTube continue de me recommander du contenu Elixir
J’aime les idées d’Erlang, mais quand j’ai dû manipuler des choses comme le serveur de chat ejabberd par le passé, ça m’avait paru trop particulier
Chaque fois que je regarde des vidéos de Joe Armstrong, je me dis que c’était un génie sous-estimé, et les processus isolés avec passage de messages, autrement dit le modèle d’acteurs, me semblent être un bon candidat pour l’avenir de la programmation distribuée
J’ai aussi été frappé de voir que, dans la vidéo sur le nouveau langage Misty de Crockford https://www.youtube.com/watch?v=R2idkNdKqpQ, les questions se sont orientées vers Elixir/Erlang
Cela dit, la « magie » de LiveView et LiveBook ne me convainc pas encore à 100 %. Ce qui paraît trop beau a généralement un piège, et les termes peu familiers comme GenServer ainsi que les connaissances propres à la plateforme donnent l’impression d’entrer dans un portail à sens unique ; je ne sais pas si ce sont des connaissances transférables ailleurs
Alors que Go a lui aussi une histoire assez solide côté concurrence, un langage et une plateforme à petite communauté comportent de gros risques en matière de recrutement, de bibliothèques et de ressources. Malgré tout, ce que la communauté Elixir a accompli est vraiment impressionnant, et j’ai hâte de voir comment l’implémentation du système de types évoluera
sendGenServer est une généralisation naturelle d’un motif que tu finiras par écrire toi-même un nombre incalculable de fois, et les connaissances sur les acteurs se transfèrent ailleurs
Il existe aussi des bibliothèques qui implémentent une abstraction d’acteurs en Go, et la boîte aux lettres d’un processus peut se voir comme une file de messages, à la manière des canaux de Go. La différence se situe plutôt dans la façon dont l’interpréteur et le yield au processeur fonctionnent
En revanche, LiveView a bien un côté magique. En réalité, beaucoup de pièces mobiles s’imbriquent, et c’est le résultat de dix ans de travail accumulé dans plusieurs communautés ; il y a beaucoup d’abstractions, mais les idées elles-mêmes sont mûres
Des choses comme Riak étaient en avance sur leur temps, et portaient presque déjà l’idée qu’on pouvait construire des systèmes distribués robustes comme on construit un GenServer
Chris et José travaillent tous deux beaucoup à accroître l’exposition en dehors du cercle initial d’Elixir, et les membres de la communauté font la même chose à leur niveau
Le recrutement n’est pas aussi compliqué qu’on pourrait le croire. Il y a pas mal de personnes qui veulent travailler avec ce langage, et les bons développeurs rattrapent leur retard assez vite
Cela dit, il faut apprivoiser le côté langage de niche. C’est un risque pour le langage lui-même, mais à court terme, être compétent dans une technologie de niche peut aussi être récompensé
On peut voir et comprendre ce que fait la plupart du code, et le lire. Avec JavaScript, ce n’est pas souvent le cas
En revanche, il est vrai que recruter et trouver des tutoriels est difficile. Par exemple, trouver quelqu’un qui a déjà fait en Elixir ce que je veux faire est beaucoup plus difficile qu’en JavaScript
Les bibliothèques n’ont pas vraiment été un gros problème, et il y en a pas mal. Quand on utilise des API, il existe parfois des bibliothèques clientes pour JavaScript ou Python, mais pas pour Elixir ; en général, utiliser l’API avec un simple client HTTP n’est pas si difficile
Si j’utilise Node.js aujourd’hui, ce n’est pas parce que j’en ai envie, mais parce que je dois le faire. Après l’avoir essayé, je me suis dit que je ne lancerais absolument jamais un nouveau projet en Node.js
Les outils de contrôle de la concurrence, de montée en charge, de fiabilité et d’observabilité de Node.js sont très loin derrière l’écosystème BEAM
À mon avis, si certains trouvent Elixir trop beau pour être crédible, c’est parfois parce qu’ils sont trop habitués à des plateformes qui n’ont pas été conçues pour la résilience
Avec LiveBook, hormis les détails exacts du calcul de diff ou de la compression des données, il est assez facile d’avoir le bon modèle mental. En cas d’urgence, on peut regarder les messages WebSocket et comprendre assez bien ce qui se passe
LiveView est étonnamment intuitif
Le grand attrait d’Elixir/Erlang, dans le contexte d’un serveur web, c’est qu’il permet de lancer des tâches en arrière-plan très facilement, sans craindre qu’une entrée-sortie bloquante fige tout le serveur
Dans un précédent poste, je devais faire beaucoup de requêtes HTTP dans un gestionnaire de webhooks, et quand il y en avait suffisamment en parallèle, tous les processus du système d’exploitation se retrouvaient occupés et tout le site tombait
Dans ces moments-là, je me disais vraiment que, si on avait été en Elixir, il aurait suffi de lancer une Task et de passer à la suite
L’idée de partager des objets entre processus asynchrones Tokio m’a paru difficile comparée au fait que ce soit une fonctionnalité de premier ordre dans Elixir. Ce n’est pas impossible ni intrinsèquement difficile, mais ça semble plus difficile à faire correctement
Je préfère placer les tâches en arrière-plan sur une capacité de calcul différente de celle des requêtes HTTP. Les profils d’utilisation des ressources sont différents, et je veux mettre un état ou une file en amont pour rendre explicites les reprises, la gestion des erreurs et la contre-pression
Bien sûr, ce serait agréable de ne pas avoir cette complexité et que le même framework s’occupe de tout, mais je pense qu’un langage de programmation ou un runtime ne peut pas aller jusque-là à lui seul. Le monitoring et les procédures d’exploitation doivent aussi faire partie de la solution
La présentation The Soul of Erlang and Elixir de Sasa Juric montre bien à quel point ce langage peut être puissant
https://www.youtube.com/watch?v=JvBT4XBdoUE&t=4
Je la recommande vivement à ceux qui ont entendu « laissez-le échouer » ou « BEAM gère vraiment bien la concurrence », mais qui n’ont pas encore eu le déclic. C’est une présentation vraiment formidable
Depuis 1991, je programme tous les jours en Erlang, et je comptais continuer ainsi
Entre-temps, j’ai aussi essayé Haskell, Rust, Elixir, etc. Le typage fort a beaucoup de valeur, mais au final tout se ramène à l’endroit où l’on prend le plus de plaisir
Erlang est le langage de programmation qui m’apporte le plus de plaisir, et je prévois de continuer à hacker en Erlang au moins jusqu’à ma retraite, dans quelques années
Cela fait un peu plus d’un an que j’ai découvert Elixir, et j’ai vraiment pris plaisir à l’apprendre et à l’intégrer dans plusieurs composants
Le plus grand plaisir vient du fait que BEAM/OTP fournit énormément de choses par défaut à lui seul
Quelqu’un de l’équipe avec laquelle je travaillais a dit un jour : « BEAM/OTP, c’est comme k8s, sans les parties compliquées »
J’ai appris Elixir pendant plusieurs années, mais la migration OpenShift — donc k8s — que j’ai récemment conçue a quelque peu atténué cet avantage
Sans k8s, la proposition aurait été bien plus séduisante. À la place, nous avons conservé la stack de développement existante et atteint, avec des concepts k8s, ce que nous aurions fait avec OTP. Les compromis étaient évidemment différents
« Elixir vous donne un langage plaisant comme Ruby, tout en retirant les pièges liés à l’état des langages orientés objet. Pas de classes, pas d’instances, pas d’héritage ; il est immuable, fonctionnel, et ne vous enferme pas dans un système de typage statique »
C’est ce que je veux, mais j’aimerais aussi avoir des types. Je suis convaincu que le typage fort est la bonne approche dans les grandes bases de code, car il met en évidence beaucoup de magie et rend le raisonnement plus facile
Grâce au pattern matching / à l’unification des fonctions, on sait assez bien quelle forme prennent les données qui entrent et sortent. Et si l’on reste inquiet, il y a Dialyzer
Dialyzer est un outil d’analyse statique pour Erlang/Elixir. Il est inclus dans la distribution standard d’Erlang, et son nom signifie « DIscrepancy AnaLYZer for ERlang programs »
Il détecte, dans un module Erlang isolé ou un ensemble d’applications, des incohérences comme des erreurs de type, du code mort ou des tests inutiles. Il infère les types à partir de programmes corrects ; les annotations de type ne sont pas obligatoires, mais lorsqu’elles sont présentes, elles permettent de produire de meilleurs avertissements
https://fly.io/phoenix-files/adding-dialyzer-without-the-pai...
https://elixir-lang.org/blog/2023/06/22/type-system-updates-...
Un GenServer ressemble à un objet : il ne s’agit pas simplement d’une structure avec des fonctions associées, il interagit via des messages
J’aimerais pouvoir définir, pour les GenServer d’un projet, un template de base avec de l’enregistrement commun, de la configuration mémoire, du logging, etc., puis le spécialiser pour trois variantes de tâches précises
Elixir comble ce manque avec de la génération de code par macros, mais ce n’est pas terrible. Mes GenServer sont remplis de petites répétitions
Dire qu’« en Ruby, il est courant d’utiliser les exceptions pour le flux de contrôle » me semble tout simplement faux
L’exemple qui suit ce paragraphe est la méthode
updatede Rails, alors que la façon utilisée dans les exemples Rails standard et les générateurs est la version deupdatequi n’utilise pas d’exceptionLes bibliothèques ou applications d’une certaine taille, lorsqu’elles deviennent suffisamment complexes, passent à des classes d’erreurs explicites. Car la gestion d’erreurs basée sur l’état, comme dans ActiveRecord, est pénible
Maintenant que Ruby dispose dans une certaine mesure du pattern matching, je pense qu’il peut remplacer les exceptions utilisées pour le flux de contrôle. Au lieu de lever une exception, on peut retourner la classe elle-même et faire un matching dessus
Beaucoup de gems utilisent les exceptions pour le flux de contrôle, et c’était courant aussi dans les apps et bibliothèques que j’ai écrites ou maintenues
Rails comporte beaucoup de méthodes
bang!qui lèvent une exception en cas d’erreur. Mais, de manière générale, en Ruby comme en Python, il est effectivement courant d’utiliser des exceptions pour le flux de contrôleJe viens de F# et je n’ai fait qu’effleurer Elixir, mais le langage me semble malheureusement un peu déroutant
Par exemple, les signatures de fonctions n’exposent que le nom et le nombre de paramètres, pas les types ; dans une grosse bibliothèque, il devient donc assez difficile de comprendre mentalement ce qui va où
On ne voit souvent qu’on a passé de mauvais paramètres qu’après avoir exécuté le code et débogué. Dans un langage à typage strict comme F#, ce genre de problème est détecté directement pendant le codage, ce qui réduit beaucoup le temps et les efforts
Du coup, en dehors de l’environnement Erlang, je me demande quel est l’attrait principal d’Elixir
https://elixir-lang.org/blog/2023/09/20/strong-arrows-gradua...
Pour l’instant, c’est bien un langage dynamique, donc on n’obtient pas beaucoup d’informations de type depuis le langage lui-même. Ce n’est pas encore un langage à typage statique
Mais ce qui, pour moi, donne réellement l’avantage à Elixir par rapport aux autres langages que j’ai utilisés, c’est BEAM/OTP
D’après mon expérience, beaucoup de fonctionnalités qu’il faudrait apporter via des bibliothèques dans d’autres langages sont incluses par défaut. Ces bibliothèques augmentent fortement la complexité et ont parfois une courbe d’apprentissage importante. Les fonctionnalités liées à l’asynchrone en sont un bon exemple
Je pense qu’après avoir utilisé Elixir un certain temps et s’être familiarisé avec ces outils et abstractions intégrés, on comprend la puissance de tout l’écosystème
L’attrait principal est probablement le fait qu’il tourne sur BEAM avec une syntaxe à la Ruby. BEAM est à mon avis un avantage assez important
Ce n’est pas vraiment comparable aux types de F#, mais on peut utiliser les typespecs : https://hexdocs.pm/elixir/1.15.7/typespecs.html
Par exemple, il existe un flux où le machine learning est intégré nativement avec Axon, Nx et BumbleBee, jusqu’à la compilation GPU : https://www.youtube.com/watch?v=HK38-HIK6NA
La communauté pousse réellement très fort dans cette direction
Le fonctionnement de LiveView est aussi séduisant. Cela dit, .NET a Blazor
Elixir peut être utilisé dans divers contextes, comme l’embarqué avec Nerves ou le scripting. À ma connaissance, F# peut aussi servir au scripting
Globalement, BEAM et les structures qui l’entourent sont ses points forts, et le gestionnaire de paquets Hex est vraiment bon. Bien sûr, .NET a aussi de bons outils
On peut s’en sortir avec les typespecs, le pattern matching et les guards, mais c’est assez fastidieux et, au moins dans le plugin JetBrains porté par la communauté, ce n’est pas très convivial pour l’IDE/IntelliSense
Il vaudrait mieux écrire ça après l’avoir utilisé pendant 2 ans, pas au moment de démarrer
Une nouveauté ressemble toujours au début à une solution miracle brillante et sans défaut
Les premières impressions ont aussi leur place, et c’est ce que je propose ici. Le public visé n’est pas composé d’experts Elixir/Erlang, mais d’experts Ruby
Elixir était déjà un joyau à l’époque et l’est toujours aujourd’hui. Il n’est pas parfait, mais il tient remarquablement bien la route
J’utilise Ruby depuis environ 2007
Chaque fois que j’ai essayé de passer à la prochaine chose « cool », j’ai fini par revenir à Ruby, parce que j’aime Ruby, que c’est avec Ruby que je travaille le plus vite, et que cela suffisait
Mais récemment, je vois beaucoup plus parler d’Elixir, et son apparence comme ses sensations me plaisent. Je vais bientôt lancer un nouveau side project et je pense que je devrais l’essayer