Catégories
javascript

Résoudre le call­back hell — Javas­cript 102

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érar­chie de réper­toires et faire un gros fichier Json qui réca­pi­tule les diffé­rentes tailles.

Faire un petit fichier outil CLI à l’aide de comman­der.js : un call­back. Lister les fichiers d’un réper­toire : un call­back. Lire les exif : un call­back. Écrire dans un fichier JSON : un call­back.

Bon, lire et écrire peuvent se faire en synchrone sans call­back – et dans mon cas précis ça ne chan­ge­rait proba­ble­ment rien – mais je suis là aussi pour apprendre comment faire pour plus tard dans cet écosys­tème.

Une hiérar­chie de quatre fonc­tions 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 trans­for­ma­tion en JSON ne me demande pas un call­back.

Me voici en plein call­back hell. Le problème est connu. J’ai bête­ment pensé qu’on me donne­rait une solu­tion rapi­de­ment, un outil ou un usage à suivre.

On me pointe vers les promesses ES2015 mais l’API Node et tous les modules en ligne conti­nuent à utili­ser des call­back. Sérieu­se­ment, personne n’a cher­ché à présen­ter l’API Node direc­te­ment en promesses ?

C’est donc à moi de trans­for­mer chaque méthode ou chaque module pour qu’il utilise des promesses. Promi­sify semble être la baguette magique pour ça. À partir de là il suffit de conver­tir chaque module pour enchaî­ner des promesses (atten­tion aux écueils).

Fran­che­ment ça reste assez moche. Des gens biens me pointent vers les systèmes à base de géné­ra­teurs (je recom­mande la lecture de ce lien) et le module co. Le code résul­tant est déjà bien plus lisible malgré les arti­fices.

On me rappelle alors les async / await (là aussi, je recom­mande la lecture). Je crois que c’est seule­ment main­te­nant, après cette explo­ra­tion, que je comprends l’in­té­rêt et le fonc­tion­ne­ment. Parfait, si ce n’est qu’il faut choi­sir presque au hasard entre trois plugins diffé­rents pour ajou­ter la fonc­tion­na­lité à Babel.

Atten­tion, ce n’est pas magique. Il faut se rappe­ler que ce n’est qu’une surcouche aux promesses, et il y a quelques écueils (lien indis­pen­sable si vous comp­tez utili­ser async/await) à bien connaitre là ici aussi, entre autres pour ne pas perdre les excep­tions dans un trou noir.

Je me retrouve avec un code qui a l’air rela­ti­ve­ment élégant mais la réalité me rattrape : Visi­ble­ment quasi aucun code en produc­tion ne fonc­tionne ainsi. Une majo­rité des gens utilisent encore des call­backs, les autres se sont conten­tés des promesses. Est-ce une bonne idée de déjà viser les async/await ?


Il reste aussi que j’ai l’im­pres­sion de retrou­ver le mode multi-tâche coopé­ra­tif de Micro­soft Windows 3.1 en moins bien inté­gré. Je ne vois aucune raison pour que la biblio­thèque stan­dard m’im­pose des call­backs, 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 proces­sus quand mon programme est en attente d’une i/o. Si la machine virtuelle du langage veut gérer plusieurs fils d’exé­cu­tion en interne pour opti­mi­ser le proces­seur, qu’elle le gère elle-même via l’API mise à dispo­si­tion. C’est à elle d’iden­ti­fier que je veux ouvrir un fichier et de savoir qu’elle peut favo­ri­ser un autre fil d’exé­cu­tion en atten­dant que le disque me remonte la donnée. Je trouve ahuris­sant que ces méca­nismes débordent sur mon code et le complexi­fient 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é­cu­tions 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 program­ma­tion et les modules proposent des call­backs partout et pour tout (à vue de nez, y compris là où ça n’a pas vrai­ment de sens, à mon humble avis par mime­tisme).

Promesses, async, call­backs… J’adore tous ces concepts, mais quand j’en ai besoin moi, pas pour compen­ser les choix d’ar­chi­tec­ture du moteur sous-jacent.

Javas­cript a énor­mé­ment évolué, dans le bon sens. Côté syntaxe ES2015 et suivant donnent un résul­tat qui m’at­tire beau­coup. Le fonc­tion­ne­ment et l’API de Node.js me semblent pour l’ins­tant gâcher tout ce résul­tat. Un beau langage avec un boulet aux pieds.

8 réponses sur « Résoudre le call­back hell — Javas­cript 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

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

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *