Après la mise en place, il est peut-être temps de faire un premier script. J’ai tenté un petit script que j’ai fait la veille en ruby : lister toutes les images dans une hiérarchie de répertoires et faire un gros fichier Json qui récapitule les différentes tailles.
Faire un petit fichier outil CLI à l’aide de commander.js : un callback. Lister les fichiers d’un répertoire : un callback. Lire les exif : un callback. Écrire dans un fichier JSON : un callback.
Bon, lire et écrire peuvent se faire en synchrone sans callback – et dans mon cas précis ça ne changerait probablement rien – mais je suis là aussi pour apprendre comment faire pour plus tard dans cet écosystème.
Une hiérarchie de quatre fonctions de rappel pour ce qui m’a pris 5 à 10 lignes tout mouillé en Ruby, ça me fait mal. J’avoue, je suis presque surpris que la transformation en JSON ne me demande pas un callback.
Me voici en plein callback hell. Le problème est connu. J’ai bêtement pensé qu’on me donnerait une solution rapidement, un outil ou un usage à suivre.
On me pointe vers les promesses ES2015 mais l’API Node et tous les modules en ligne continuent à utiliser des callback. Sérieusement, personne n’a cherché à présenter l’API Node directement en promesses ?
C’est donc à moi de transformer chaque méthode ou chaque module pour qu’il utilise des promesses. Promisify semble être la baguette magique pour ça. À partir de là il suffit de convertir chaque module pour enchaîner des promesses (attention aux écueils).
Franchement ça reste assez moche. Des gens biens me pointent vers les systèmes à base de générateurs (je recommande la lecture de ce lien) et le module co. Le code résultant est déjà bien plus lisible malgré les artifices.
On me rappelle alors les async / await (là aussi, je recommande la lecture). Je crois que c’est seulement maintenant, après cette exploration, que je comprends l’intérêt et le fonctionnement. Parfait, si ce n’est qu’il faut choisir presque au hasard entre trois plugins différents pour ajouter la fonctionnalité à Babel.
Attention, ce n’est pas magique. Il faut se rappeler que ce n’est qu’une surcouche aux promesses, et il y a quelques écueils (lien indispensable si vous comptez utiliser async/await) à bien connaitre là ici aussi, entre autres pour ne pas perdre les exceptions dans un trou noir.
Je me retrouve avec un code qui a l’air relativement élégant mais la réalité me rattrape : Visiblement quasi aucun code en production ne fonctionne ainsi. Une majorité des gens utilisent encore des callbacks, les autres se sont contentés des promesses. Est-ce une bonne idée de déjà viser les async/await ?
Il reste aussi que j’ai l’impression de retrouver le mode multi-tâche coopératif de Microsoft Windows 3.1 en moins bien intégré. Je ne vois aucune raison pour que la bibliothèque standard m’impose des callbacks, des promesses ou des async/await à tout bout de champ plutôt que de gérer ça en interne.
L’OS sait déjà passer à un autre processus quand mon programme est en attente d’une i/o. Si la machine virtuelle du langage veut gérer plusieurs fils d’exécution en interne pour optimiser le processeur, qu’elle le gère elle-même via l’API mise à disposition. C’est à elle d’identifier que je veux ouvrir un fichier et de savoir qu’elle peut favoriser un autre fil d’exécution en attendant que le disque me remonte la donnée. Je trouve ahurissant que ces mécanismes débordent sur mon code et le complexifient ainsi.
Oui, les promesses ne servent pas qu’à gérer la lenteur des i/o et faire coopérer les différents fils d’exécutions de la machine virtuelle V8 mais c’est quand même pour ça qu’on me les impose partout dans l’API de Node.js. Ça en devient même un style de programmation et les modules proposent des callbacks partout et pour tout (à vue de nez, y compris là où ça n’a pas vraiment de sens, à mon humble avis par mimetisme).
Promesses, async, callbacks… J’adore tous ces concepts, mais quand j’en ai besoin moi, pas pour compenser les choix d’architecture du moteur sous-jacent.
Javascript a énormément évolué, dans le bon sens. Côté syntaxe ES2015 et suivant donnent un résultat qui m’attire beaucoup. Le fonctionnement et l’API de Node.js me semblent pour l’instant gâcher tout ce résultat. Un beau langage avec un boulet aux pieds.
8 réponses à “Résoudre le callback hell — Javascript 102”
As-tu regardé du coté des générateur avec yield?
Je te conseil de tester js-csp qui reprend les concepts de channel comme en Go.
oui oui, il y a un paragraphe avec un lien à ce propos, plus la référence vers « co » qui est fait pour ça. Les async/await sont faits pour ça et intégrés dans le langage.
js-csp m’a l’air plus complexe et moins intégré, peut-être simplement pas mon besoin actuel (gérer les callbacks de l’API Nodejs). Je jetterai un second coup d’oeil ces prochains jours
Certains me demandaient les codes sources pour commenter et comprendre. J’ai tenté de mieux détailler mon exploration 102 ici : https://gist.github.com/edas/8c1f5b351732d499d8c7d3c3c1d80435
Commentaires bienvenus (lire mon commentaire sur le gist avant quoi que ce soit)
Je ne connaissais pas promisify, c’est très intéressant !
Même si la version async/await semble la plus intéressante, trois remarques:
– Il manque une gestion d’erreur fine. Ça peut paraître anodin mais ça influence beaucoup le choix d’une solution agréable. Par exemple, si tu n’as pas les droits de lectures sur un fichier, tu fais quoi ? Tu écris le manifeste sans ce fichier ? Tu écris le manifeste avec ce fichier mais sans les dimensions ? Tu n’écris pas le manifeste ? En fonction du comportement du programme, tu vas avoir une solution qui penche plus pour telle solution ou une autre. Mais clairement, tu n’auras pas qu’un seul try/catch.
– Comme tu le dis, async/await n’est pas supporté par node aujourd’hui, ce qui oblige une étape de compilation. Je trouve la version avec les promises pas si moche que ça donc si async/await est la seule raison pour une compilation, je resterai avec les promises.
– Toutes tes solutions n’utilisent qu’une approche. Comme le dit l’expression, [There is no silver bullet](https://en.wikipedia.org/wiki/No_Silver_Bullet). Ma version préférée reste async/await avec un `Promise.all`. J’ai donc laissé [une version](https://gist.github.com/edas/8c1f5b351732d499d8c7d3c3c1d80435#gistcomment-1847785) sur le gist.
Async/Await permet de mettre des try/catch où tu veux. Je me suis contenté d’une gestion d’erreur assez ridicule sur tous les scripts. Tu peux effectivement faire plus fin si tu le souhaites, mais souhaiteras-tu vraiment faire une gestion différente pour chaque appel qui peut casser ? Pas certain non plus.
En fait c’est la gestion des erreurs sur les promesses qui me fait poser le plus de questions mais j’ai besoin de plus me renseigner avant d’avoir un jugement.
Gérer les ios asynchrones dans le code permet d’éviter le coût de spawn d’un processus et la mémoire que ça occupe. Après il y a l’approche de Go qui me semble supérieure mais difficile d’ajouter ça en JS sans péter la compatibilité ascendante.
Il y a plein de façon de gérer le multi-thread. Les deux options classiques sont les processus lourds ou léger (certains diront les processus et les threads). Les deux sont plus ou moins gérés par le système. Ça demande des changements de contexte, ça bouffe donc de la performance, mais pas toujours au point où on le pense. C’est quand même comme ça que se dimensionnent la plupart des serveurs applicatifs aujourd’hui.
Après on peut gérer ça à la main dans le processus. Il y a trois zillions de solutions. Il y a l’event loop, les modèles d’acteurs, les CSP, …
Chaque méthode a forcément des impacts sur le style de programmation mais j’avoue que là je trouve l’impact fort dans le cas de Nodejs. Oui, si on veut changer de modèle il faut réécrire la lib standard de Nodejs, donc ça n’est pas prêt d’arriver je pense.
Un lien : http://java-is-the-new-c.blogspot.fr/2014/01/comparision-of-different-concurrency.html
Un billet similaire en anglais : https://philipwalton.com/articles/untangling-deeply-nested-promise-chains/