Je viens de retomber sur cet article dont j'avais déjà parlé brièvement ici il y a 3 ans. Il se trouve que cet article est sans doute l'un des plus influents articles parlant de développement que j'ai pu voir passer ces dernières années, en fait il est tellement influent qu'il est assez difficile de passer 6 mois sans tomber sur au moins un message de forum qui cite cet article comme argument. Et c'est dommage, parce que cet article est franchement douteux. En fait j'aurai aujourd'hui tendance à penser que son argumentation est complètement idiote. Le problème, c'est qu'il est très bien écrit, avec une analogie très puissante, ce qui le rend viral. Et quand des raisonnements bêtes deviennent viral, c'est assez triste … (Un autre exemple classique c'est le très célèbre article d'économie The Market For “Lemons”, qui est tellement influent que ça a valu à son auteur un «prix Nobel d'économie» alors que le raisonnement en lui-même est complètement idiot, à tel point qu'il arrive quand même à conclure que «le marché du véhicule d'occasion ne peut pas exister sans réglementation pour l'encadrer» … Et ce, à une époque où il existe un marché du véhicule d'occasion, et pas encore de réglementation !).
Mais revenons à notre sujet de départ, pourquoi cet article est-il douteux ? Déjà, arrêtez tout et allez lire l'article, il n'est pas si long, vous n'en aurez pas pour longtemps.
En fait, le principal problème de cet article, c'est que c'est en fait un article de «JavaScript-bashing»[1], critiquant le JavaScript à l'ancienne qui plus est (l'article date de 2015), avant l'apparition de la syntaxe async
/await
, déguisé en un argument général contre le paradigme de programmation asynchrone. Sauf que, autant le vieux JavaScript à l'ancienne, plein de callbacks imbriqué, c'est légitime de le critiquer, autant généraliser ça à l'ensemble du monde asynchrone ça ne l'est pas. Et c'est malheureusement comme ça que cet article a été compris par la quasi-totalité des gens qui le citent (la plupart du temps, des adorateurs béats du langage Go, on comprend aisément pourquoi).
Petit tour des critiques énoncées dans l'article :
Point 1 :
Imagine a “blue call” syntax and a “red call” syntax. Something like:
doSomethingAzure(...)•blue;
doSomethingCarnelian()•red;
When calling a function, you need to use the call that corresponds to its color. If you get it wrong—call a red function with •blue after the parentheses or vice versa—it does something bad.
Point 2 :
You can call a blue function from with a red one. This is kosher:
red•function doSomethingCarnelian() { doSomethingAzure()•blue; }
But you can’t go the other way. If you try to do this:
blue•function doSomethingAzure() { doSomethingCarnelian()•red; }
Well, you’re gonna get a visit from old Spidermouth the Night Clown.
Point 3 :
Red functions are more painful to call.
Et ces critiques, elles sont vraiment liées au JavaScript, et particulièrement au JavaScript old-school. Le point 3 par exemple, était vrai quand asynchronisme impliquait toute la cascade de callbacks imbriqués les uns dans les autres, mais ne l'est plus du tout aujourd'hui, grâce à la syntaxe async
`await`. Et même à l'époque, les callbacks imbriqués c'était pénible (le fameux “callback hell”) mais manifestement pas suffisamment pour empêcher NodeJs de devenir l'une des plate-formes de développement back-end les plus populaires !
Quant aux points 1 et 2, ils disparaissent complètement si on sort du cadre du JavaScript : dans le cas d'un langage typé statiquement, appeler une fonction asynchrone en pensant qu'elle est synchrone provoque juste une erreur de type à la compilation. Et si on se place dans un environnement qui n'est pas du JavaScript, et qui du coup dispose de plusieurs threads, il est très facile de transformer une fonction asynchrone en fonction synchrone (en bloquant juste le thread jusqu'à ce que le résultat soit résolu) et réciproquement, de transformer une fonction synchrone en fonction asynchrone l'aide d'un thread-pool.
Dans la toute dernière partie, l'auteur s'écarte un peu du JavaScript, et tente de reprendre un peu de hauteur en regardant d'autres langages (notamment C#, qui a l'époque a déjà la syntaxe async
/await
), et les solutions qui s'y trouvent, puis formule cette sentence générale :
Async-await is nice, which is why we’re adding it to Dart. It makes it a lot easier to write asynchronous code. You know a “but” is coming. It is. But… you still have divided the world in two. Those async functions are easier to write, but they’re still async functions. You’ve still got two colors. […] It is better. I will take async-await over bare callbacks or futures any day of the week. But we’re lying to ourselves if we think all of our troubles are gone. As soon as you start trying to write higher-order functions, or reuse code, you’re right back to realizing color is still there, bleeding all over your codebase.
Et c'est probablement ce passage là qui a valu à cet article son succès et son rayonnement : cette déclaration solennelle exhibant un danger lattent nous menaçant tous, bien au-delà du JavaScript.
Le principal problème de cette «déclaration de guerre contre le terrorisme asynchrone» c'est qu'elle s'appuie très fortement sur l'exemple initial du JavaScript, et le malaise généré, pour prétendre qu'il y a un problème avec le fait d'avoir deux types de fonctions. Sauf qu'on l'a vu, si vous ne faites pas du JavaScript (et que vous faites par exemple du C# ou du Rust) la cohabitation entre code synchrone et code asynchrone n'est pas du tout un problème, puisqu'elle ne présente aucune des 3 difficultés existant dans le JavaScript à l'ancienne.
Et le pire, c'est ce qui va suivre :
Three more languages that don’t have this problem: Go, Lua, and Ruby.
Any guess what they have in common?
Threads
Et oui, après avoir expliqué combien le monde asynchrone était difficile à utiliser, en s'appuyant sur le langage qui gérait ça le moins bien de tous, voici notre très cher auteur qui nous explique que la solution ce sont les THREADS. Oui, la plus ancienne primitive de programmation concurrente de l'histoire, oui celle qui vous expose à des deadlock, à des race-conditions et qui vous incite à utiliser des montages complètement délirants à base de channels (je vous recommande vraiment chaudement de cliquer sur ce lien) pour résoudre vos problèmes …
Sachant en plus qu'en pratique, les threads ne font que masquer la différence entre les «deux types de fonctions»: au lieu de laisser le développeur gérer cette différence et prendre les bonnes décisions en fonction, elle laisse reposer ce choix sur le runtime qui gère les threads, impliquant des grosses contraintes d'implémentations sur le runtime en question, avec leur lot de bugs (c.f. en Go).
It’s that Go has eliminated the distinction between synchronous and asynchronous code.»
<troll>
Oui, en rendant tout synchrone, et en obligeant le développeur à revenir à des paradigmes de programmation du siècle dernier. Ce qui n'est pas du tout surprenant de la part d'un vieux réac comme Rob Pike tout droit sorti des années 70 … </troll>
En réalité, le monde de l'informatique a fait un progrès ces dernières années en réalisant que les deux types de situations existent : il a des fonctions pour lesquelles on a le résultat directement et des fonctions pour lesquelles on doit attendre avant d'avoir le résultat, et pendant ce temps on peut faire autre chose. Ce n'est pas une question de langage, c'est le fonctionnement même d'un ordinateur, où certaines choses sont limitées par le CPU et d'autres par des ressources externes plus lentes. Anciennement, on traitait toutes les fonctions de la même façon: en attendant son résultat et en ne faisant rien tant qu'il n'était pas là. On s'est très vite rendu compte qu'en procédant ainsi on gâchait du CPU, et on a introduit le principe de fil d’exécution (les threads), qu'on pouvait essayer de faire avancer plus ou moins en parallèle, en utilisant les pauses d'un thread pour faire avancer l'autre. Ce mécanisme nécessite l'utilisation d'un ordonnanceur (scheduler) qui essaye automatiquement de déterminer quel thread doit avancer quand. De plus, dans le cas d'un programme ayant plusieurs appels lents à exécuter en même temps, le développeur doit manuellement gérer un thread par appel lent, et synchroniser ceux-ci pour reprendre le travail sur le thread principal lorsque leurs différentes actions lentes sont terminées.
Plus récemment, on a vu apparaître des constructions syntaxiques dans les langages de programmation qui permettent de gérer la différence entre actions lentes et actions rapide directement au niveau de l'écriture du programme, plutôt que de faire comme si toutes les actions étaient de même nature et de compter sur le runtime (qui peut être le système d'exploitation lui-même) pour gérer la différence entre les deux.
Vouloir effacer cette différence fondamentale entre action lente et action rapide et revenir aux threads, c'est clairement un retour en arrière historique. Qu'heureusement, personne ne semblent vraiment motivé à faire, à part les créateurs de Go bien évidemment.
[1] c'est même plus précisément un article de NodeJs-bashing, ce que l'auteur reconnaît bien volontiers dans la 2ème partie de l'article.