Question

J'ai une liste de méthodes que je fais appel à 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 someOtherObject.doASecondThing() comme étant asynchrone, et je pourrais retourner doOneThing asynchrones trop.Je pourrais utiliser une fonction de rappel et appel.doSomethingElse de l'intérieur de la fonction de rappel:

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

Cependant, parce que la séquence est en croissance, il semble un peu en désordre pour avoir des rappels de s'appeler les uns les autres, pour une raison quelconque, il rend la séquence a pas l'air évident comme avant, et l'indentation peut croître avec le nombre de méthodes appelées dans la séquence.

Est-il un moyen de le rendre meilleur?Je pourrais utiliser le pattern observer aussi mais il ne rend pas les choses très évident non plus, à mon avis.

Merci,

Était-ce utile?

La solution

Les Continuations, et pourquoi ils sont à l'origine de rappel spaghetti

L'écriture des rappels vous oblige à écrire dans un moment semblable à "la poursuite de passage de style" (CPS), un outil extrêmement puissant mais difficile technique.Il représente un total d'inversion de contrôle, littéralement tourner un calcul "à l'envers".CPS rend votre code de la structure de mentionner explicitement le flux de contrôle du programme (parfois une bonne chose, parfois, une mauvaise chose).En effet, vous êtes explicitement écrit en bas de la pile de fonctions anonymes.

Comme une condition préalable pour comprendre cette réponse, vous pouvez trouver 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 êtes plaint, ce qui en fait assez en retrait de code, parce que les fermetures sont une façon naturelle de s'emparer de cette pile.


Les monades à la rescousse

Une façon de unindent CPS est d'écrire "monadically" comme en Haskell.Comment pourrions-nous le faire?Une belle façon de mettre en œuvre les monades en javascript est avec le point-le chaînage de notation, semblable à jQuery.(Voir http://importantshock.wordpress.com/2009/01/18/jquery-is-a-monad/ pour un divertissement amusant.) Ou nous pouvons utiliser la réflexion.

Mais d'abord, nous avons besoin d'une manière "d'écrire en bas de la plomberie", et ALORS nous pouvons trouver une façon abstraite loin.Tragiquement, c'est un peu dur d'écrire un générique de l'errance de la syntaxe du javascript, donc je vais utiliser des listes pour représenter les 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 laid.Mais nous pouvons faire de cette UNINDENTED "code" de travail.Nous pouvons vous soucier de faire encore mieux plus tard (dans la dernière section).Ici, notre but était d'écrire tous les "informations nécessaires".Nous aimerions un moyen facile d'écrire chaque "ligne", avec un contexte, on peut les écrire dans.

Maintenant, nous mettons en œuvre une generateComputation ce qui génère de la imbriqués les fonctions anonymes qui permettrait d'effectuer les étapes ci-dessus dans l'ordre si nous l'avons exécuté.C'est ce que cette mise en œuvre pourrait ressembler à:

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];  // e.g. step #3
                var stepFunction = step[1];  // e.g. func: plus
                var stepArgs     = step[2];  // e.g. 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;
}

Démonstration:

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

sidenote: reduceRight la sémantique implique les étapes sur le droit sera de plus en plus profondément imbriquée dans les fonctions (dans l'avenir).Pour info pour ceux qui ne connaissent pas, [1,2,3].reduce(f(_,_), x) --> f(f(f(0,1), 2), 3), et reduceRight (en raison de la mauvaise considérations de conception) est équivalent à [1.2.3].reversed().reduce(...)

Ci-dessus, generateComputation faites un tas de fonctions imbriquées, les enveloppant dans un autre en préparation, et lors de l'évaluation avec ...(alert), non pelées un par un pour se nourrir dans le calcul.

sidenote:Nous devons utiliser un hack parce que dans l'exemple précédent, nous avons utilisé les fermetures et les noms de variables à mettre en œuvre de la CPS.Javascript ne permet pas suffisamment de réflexion pour le faire sans recourir à la rédaction d'une chaîne et evaling il (ick), donc nous éloigne du style fonctionnel temporairement et d'opter pour la mutation d'un objet qui conserve la trace de tous les paramètres.Ainsi, le ci-dessus de plus près la réplique suivante:

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.
                            )
                        }
                    )
                }
            )

        }
    )
}

Idéal syntaxe

Mais on a encore envie d'écrire des fonctions dans un façon saine.Comment pourrions-nous idéalement envie d'écrire notre code pour profiter de la CPS, mais tout en conservant notre santé mentale?Il existe de nombreux prend dans la littérature (par exemple, Scala shift et reset les opérateurs ne sont qu'une des nombreuses façons de le faire), mais pour la santé mentale de souci, nous allons trouver un moyen de rendre sucre syntaxique régulier de la CPS.Il existe quelques façons 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 dans une chaîne, on peut facilement se nourrir l'un dans l'autre sans se soucier de nommage.Ces fonctions ont un seul argument:le rappel de l'argument.(Si ils ne l'ont pas, vous pourriez au curry la fonction comme suit, sur la dernière ligne). Ici, nous pouvons utiliser jQuery style dot-chaînes.

 

// 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 rappels forme d'un arbre de dépendances, nous pouvons également obtenir loin avec jQuery style dot-chaînes, mais cela irait à l'encontre de l'objectif de la création d'un monadique de la syntaxe pour la CPS, qui est pour aplatir les fonctions imbriquées.Ainsi, nous n'entrerons pas ici dans les détails.

  • Si les rappels forme d'une dépendance graphe acyclique (par exemple, 2*x+3*x, où x est utilisé deux fois), nous aurions besoin d'un moyen de nom les résultats intermédiaires de quelques rappels.C'est là que ça devient intéressant.Notre objectif est d'essayer d'imiter la syntaxe http://en.wikibooks.org/wiki/Haskell/Continuation_passing_style avec ses do-mention "déballe" et "rewraps" fonctions dans et hors de la CPS.Malheureusement, l' [1, thrice,[_x]] la syntaxe est la plus proche que nous pourrions obtenir plus facilement que (et même pas proche).Vous pourriez code dans une autre langue et de les compiler en javascript, ou à l'aide de la fonction eval (file d'attente à la sinistre de la musique).Un peu exagéré.Des solutions de rechange serait d'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écrit.Tout d'abord vous adapter à utiliser des noms logiques ('leftPart', etc.) plutôt que des chiffres.Ensuite, vous faites vos fonctions réellement paresseux objets 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 pouvez faire cela de façon automatisée par une sorte de décorateur, pas manuellement.)

sidenote:Toutes vos fonctions de rappel devrait suivre le même protocole sur l'endroit où le rappel paramètre.Par exemple, si vos fonctions de commencer avec myFunction(callback, arg0, arg1, ...) ou myFunction(arg0, arg1, ..., callback) ils pourraient ne pas être trivialement compatible, mais si elles ne sont pas sans doute, vous pourriez faire un javascript réflexion hack pour regarder le code source de la fonction et de la regex, et donc de ne pas avoir à s'en soucier.

Pourquoi passer par tous les ennuis?Cela vous permet de mélanger setTimeouts et prompts et les requêtes ajax sans souffrir de "retrait de l'enfer".Vous bénéficiez également de tout un tas d'autres avantages (comme le fait de pouvoir écrire un 10 ligne non déterministe-recherche solveur de sudoku, et la mise en œuvre arbitraire de flux de contrôle des opérateurs) qui je ne vais pas entrer dans les ici.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top