Appels séquentiels de méthodes de manière asynchrone

J'ai une liste de méthodes que j'appelle dans une méthode, comme suit:

this.doOneThing(); someOtherObject.doASecondThing(); this.doSomethingElse(); 

Lorsque cela est synchrone, ils sont exécutés l'un après l'autre, ce qui est nécessaire. Mais maintenant, j'ai SomeObjetObject.doASecondThing () comme asynchrone, et je pourrais aussi convertir DoOneThing comme async. Je pourrais utiliser un rappel et appeler that.doSomethingElse de l'intérieur du rappel:

 var that = this; this.doOneThing( function () { someOtherObject.doASecondThing(function () { that.doSomethingElse(); }); }); 

Cependant, comme la séquence augmente, il semble un peu désordonné d'appeler les rappels, pour une raison quelconque, la séquence ne semble pas aussi évidente que précédemment et l'indentation pourrait se développer avec le nombre de méthodes appelées dans la séquence.

Y a-t-il un moyen de faire en sorte que cela ressemble mieux? Je pourrais également utiliser le modèle d'observateur, mais cela ne rend pas tout à fait évident non plus, à mon avis.

Merci,

Les continuations, et pourquoi ils provoquent des spaghettis de rappel

L'écriture de rappels vous oblige à écrire en quelque temps semblable au "style de passage continu" (CPS), une technique extrêmement puissante mais difficile. Il représente une inversion totale du contrôle, transformant littéralement un calcul "à l'envers". CPS fait que la structure de votre code reflète explicitement le flux de contrôle de votre programme (parfois une bonne chose, parfois une mauvaise chose). En effet, vous écrivez explicitement la pile hors des fonctions anonymes.

Comme condition préalable à la compréhension de cette réponse, vous trouverez cela utile:

http://matt.might.net/articles/by-example-continuation-passing-style/

Par exemple, c'est ce que vous faites:

 function thrice(x, ret) { ret(x*3) } function twice(y, ret) { ret(y*2) } function plus(x,y, ret) { ret(x+y) } function threeXPlusTwoY(x,y, ret) { // STEP#1 thrice(x, // Take the result of thrice(x)... function(r1) { // ...and call that r1. // STEP#2 twice(y, // Take the result of twice(y)... function(r2) { // ...and call that r2. // STEP#3 plus(r1,r2, // Take r1+r2... ret // ...then do what we were going to do. ) } ) } ) } threeXPlusTwoY(5,1, alert); //17 

Comme vous vous en êtes plaint, cela donne un code assez indenté, car les fermetures sont la manière naturelle de capturer cette pile.


Des monades à la rescousse

L'une des façons de CPS indésirable est d'écrire «monadiquement» comme dans Haskell. Comment faisons-nous cela? Une bonne façon d'implémenter des mémades en javascript est avec la notation de chaînage des points, similaire à jQuery. (Voir http://importantshock.wordpress.com/2009/01/18/jquery-is-a-monad/ pour un détournement amusant.) Ou on peut utiliser la réflexion.

Mais d'abord, nous avons besoin d'un moyen d'écrire «la plomberie», et ensuite, nous pouvons trouver un moyen d'abstraire. Tragiquement, il est difficile d'écrire une syntaxe générique de monad en javascript, alors je vais utiliser des listes pour représenter des calculs.

 // switching this up a bit: // it's now 3x+2x so we have a diamond-shaped dependency graph // OUR NEW CODE var _x = 0; var steps = [ [0, function(ret){ret(5)},[]], //step0: [1, thrice,[_x]], //step1: thrice(x) [2, twice,[_x]], //step2: twice(x) [3, plus,[1, 2]] //step3: steps[1]+steps[2] * ] threeXPlusTwoX = generateComputation(steps); //*this may be left ambiguous, but in this case we will choose steps1 then step2 // via the order in the array 

C'est un peu moche. Mais nous pouvons faire fonctionner ce "code" UNINDENTED. Nous pouvons nous inquiéter de la rendre plus jolie plus tard (dans la dernière section). Notre objectif était d'écrire toutes les "informations nécessaires". Nous voudrions un moyen simple d'écrire chaque "ligne", avec un contexte dans lequel nous pouvons les écrire.

Maintenant, nous implémentons un generateComputation qui génère des fonctions anonymes imbriquées qui effectueraient les étapes ci-dessus dans l'ordre si nous l'avons exécuté. C'est à quoi ressemblerait une telle implémentation:

 function generateComputation(steps) { /* * Convert {{steps}} object into a function(ret), * which when called will perform the steps in order. * This function will call ret(_) on the results of the last step. */ function computation(ret) { var stepResults = []; var nestedFunctions = steps.reduceRight( function(laterFuture, step) { var i = step[0]; // eg step #3 var stepFunction = step[1]; // eg func: plus var stepArgs = step[2]; // eg args: 1,2 console.log(i, laterFuture); return function(returned) { if (i>0) stepResults.push(returned); var evalledStepArgs = stepArgs.map(function(s){return stepResults[s]}); console.log({i:i, returned:returned, stepResults:stepResults, evalledStepArgs:evalledStepArgs, stepFunction:stepFunction}); stepFunction.apply(this, evalledStepArgs.concat(laterFuture)); } }, ret ); nestedFunctions(); } return computation; } 

Manifestation:

 threeXPlusTwoX = generateComputation(steps)(alert); // alerts 25 

Sidenote: reduceRight sémantique reduceRight implique que les étapes à droite seront plus profondément ancrées dans les fonctions (plus loin dans le futur). FYI pour ceux qui ne sont pas familiers, [1,2,3].reduce(f(_,_), x) --> f(f(f(0,1), 2), 3) et reduceRight (en raison de Mauvaises considérations de conception) est en fait équivalent à [1.2.3].reversed().reduce(...)

Ci-dessus, generateComputation créé un tas de fonctions imbriquées, les enroulant les unes les autres en préparation et, lorsqu'elles sont évaluées avec ...(alert) , ne les ont pas déroulées un par un pour alimenter le calcul.

Sidenote: Nous devons utiliser un hack parce que dans l'exemple précédent, nous avons utilisé des fermetures et des noms de variables pour implémenter CPS. Javascript ne permet pas une réflexion suffisante pour le faire sans recourir à l'élaboration d'une chaîne et à l' eval (ick), de sorte que nous évitons temporairement le style fonctionnel et optons pour la mutation d'un objet qui surveille tous les paramètres. Ainsi, ce qui précède réplique plus en détail ce qui suit:

 var x = 5; function _x(ret) { ret(x); } function thrice(x, ret) { ret(x*3) } function twice(y, ret) { ret(y*2) } function plus(x,y, ret) { ret(x+y) } function threeXPlusTwoY(x,y, ret) { results = [] _x( return function(x) { results[0] = x; thrice(x, // Take the result of thrice(x)... function(r1) { // ...and call that r1. results[1] = r1; twice(y, // Take the result of twice(y)... function(r2) { // ...and call that r2. results[2] = r2; plus(results[1],results[2], // Take r1+r2... ret // ...then do what we were going to do. ) } ) } ) } ) } 

Syntaxe idéale

Mais nous voulons toujours écrire des fonctions de manière judicieuse. Comment aimerions-nous idéalement écrire notre code pour profiter de CPS, mais tout en conservant notre santé mentale? Il existe de nombreuses reprises dans la littérature (par exemple, les opérateurs de shift et de reset de Scala ne sont qu'une des nombreuses façons de le faire), mais pour des raisons de santé, nous allons simplement trouver un moyen de faire du sucre syntaxique pour un CPS régulier. Il existe des façons possibles de le faire:

 // "bad" var _x = 0; var steps = [ [0, function(ret){ret(5)},[]], //step0: [1, thrice,[_x]], //step1: thrice(x) [2, twice,[_x]], //step2: twice(x) [3, plus,[1, 2]] //step3: steps[1]+steps[2] * ] threeXPlusTwoX = generateComputation(steps); 

…devient…

  • Si les rappels sont en chaîne, nous pouvons facilement alimenter l'un vers l'autre sans nous soucier de nommer. Ces fonctions n'ont qu'un argument: l'argument de rappel. (Si elles ne l'ont pas fait, vous pouvez curry la fonction comme suit sur la dernière ligne.) Ici, nous pouvons utiliser les chaînes de points style jQuery.
 // SYNTAX WITH A SIMPLE CHAIN // ((2*X) + 2) twiceXPlusTwo = callbackChain() .then(prompt) .then(twice) .then(function(returned){return plus(returned,2)}); //curried twiceXPlusTwo(alert); 
  • Si les renvois forment un arbre de dépendance, nous pouvons également nous échapper avec des chaînes de point de type jQuery, mais cela compromettrait le but de créer une syntaxe monadique pour CPS, qui est d'aplatir les fonctions imbriquées. Nous ne parlerons donc plus ici.

  • Si les rappels forment un graphique acyclique de dépendance (par exemple, 2*x+3*x , où x est utilisé deux fois), nous aurions besoin d'un moyen de nommer les résultats intermédiaires de certains rappels. C'est là que ça devient intéressant. Notre objectif est d'essayer d'imiter la syntaxe à l' adresse http://en.wikibooks.org/wiki/Haskell/Continuation_passing_style avec sa "notation" qui "déroule" et "remplace" les fonctions dans et hors de CPS. Malheureusement, la syntaxe [1, thrice,[_x]] était la plus proche que nous pouvions facilement atteindre (et même pas proche). Vous pouvez coder dans une autre langue et compiler sur javascript, ou utiliser eval (faire la queue de la musique sinistre). Un peu trop. Les alternatives devraient utiliser des chaînes, telles que:

 // SUPER-NICE SYNTAX // (3X + 2X) thriceXPlusTwiceX = CPS({ leftPart: thrice('x'), rightPart: twice('x'), result: plus('leftPart', 'rightPart') }) 

Vous pouvez le faire avec seulement quelques ajustements à la generateComputation j'ai décrite. D'abord, vous l'adaptez pour utiliser les noms logiques ( 'leftPart' , etc.) plutôt que les nombres. Ensuite, vous créez vos fonctions en fait des objets paresseux qui se comportent comme:

 thrice(x).toListForm() == [<real thrice function>, ['x']] or thrice(x).toCPS()(5, alert) // alerts 15 or thrice.toNonCPS()(5) == 15 

(Vous ferez cela de manière automatisée avec une sorte de décorateur, pas manuellement.)

Sidenote: Toutes vos fonctions de rappel doivent suivre le même protocole sur l'endroit où le paramètre de rappel est. Par exemple, si vos fonctions commencent par myFunction(callback, arg0, arg1, ...) ou myFunction(arg0, arg1, ..., callback) ils peuvent ne pas être trivialement compatibles, mais s'ils ne sont pas vraisemblables, vous pourriez faire un Javascript reflection hack pour regarder le code source de la fonction et le regex, et ne doit donc pas vous en soucier.

Pourquoi subir tous ces problèmes? Cela vous permet de mélanger dans setTimeout s et prompt s et ajax requêtes sans souffrir de "sang-froid". Vous obtenez également un tas d'autres avantages (comme être en mesure d'écrire un solveur de sudoku de recherche non déterministe de 10 lignes et de mettre en œuvre des opérateurs de flux de contrôle arbitraires), que je n'entendrai pas ici.