3

次のように、メソッドで呼び出しているメソッドのリストがあります。

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

これが同期の場合、それらは次々に実行されますが、これは必須です。しかし、今は someOtherObject.doASecondThing() を非同期として持っているので、doOneThing も非同期にするかもしれません。コールバックを使用して、コールバック内から that.doSomethingElse を呼び出すことができます。

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

ただし、シーケンスが成長しているため、コールバックが相互に呼び出すのは少し面倒です。何らかの理由で、シーケンスが以前ほど明確に見えなくなり、シーケンスで呼び出されるメソッドの数に応じてインデントが大きくなる可能性があります。

これをより良く見せる方法はありますか?オブザーバーパターンも使用できますが、私の意見では、それも物事をあまり明白にしません。

ありがとう、

4

1 に答える 1

2

継続と、それらがコールバック スパゲッティを引き起こしている理由

コールバックを記述すると、「継続渡しスタイル」(CPS) に似たものを記述することが強制されます。これは、非常に強力ですが難しい手法です。これは制御の完全な逆転を表し、文字通り計算を「逆さま」にします。CPS は、コードの構造がプログラムの制御フローを明示的に反映するようにします (良いことも悪いこともあります)。実際には、無名関数からスタックを明示的に書き留めています。

この回答を理解するための前提条件として、これが役立つ場合があります。

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

たとえば、これはあなたがしていることです:

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

あなたが不満を言っているように、クロージャーはこのスタックをキャプチャする自然な方法であるため、これはかなりインデントされたコードになります。


救助のためのモナド

CPS のインデントを解除する 1 つの方法は、Haskell のように「モナド的に」書くことです。どうすればそれができますか?JavaScript でモナドを実装する良い方法の 1 つは、jQuery に似たドット連鎖表記を使用することです。(面白い転換については、http://importantshock.wordpress.com/2009/01/18/jquery-is-a-monad/を参照してください。) または、リフレクションを使用することもできます。

しかし、まず「配管を書き留める」方法が必要です。それから、それを抽象化する方法を見つけることができます。残念なことに、javascript で一般的なモナド構文を書くのはちょっと難しいので、リストを使って計算を表現します。

 

// 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

それはちょっと醜いです。しかし、この UNINDENTED の「コード」を機能させることができます。後で(最後のセクションで)よりきれいにすることについて心配することができます。ここでの目的は、すべての「必要な情報」を書き留めることでした。各「行」を簡単に記述できる方法と、それらを記述できるコンテキストが必要です。

ここで、generateComputation実行した場合に上記の手順を順番に実行するネストされた無名関数を生成する を実装します。これは、そのような実装がどのように見えるかです:

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;
}

デモンストレーション:

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

補足:reduceRightセマンティクスは、右側のステップが関数内でより深くネストされることを意味します (将来的に)。なじみのない人のための参考までに、[1,2,3].reduce(f(_,_), x) --> f(f(f(0,1), 2), 3)、およびreduceRight(設計上の考慮事項が不十分なため) は、実際には と同等です[1.2.3].reversed().reduce(...)

上記でgenerateComputationは、一連のネストされた関数を作成し、準備のためにそれらを互いにラップし、 で評価すると...(alert)、それらを 1 つずつ剥がして計算にフィードしました。

補足: 前の例ではクロージャーと変数名を使用して CPS を実装したため、ハックを使用する必要があります。evalJavascript では、文字列を作成してそれを ing (ick)せずにこれを行うのに十分なリフレクションが許可されていないため、一時的に機能的なスタイルを避け、すべてのパラメーターを追跡するオブジェクトを変更することを選択します。したがって、上記は以下をより厳密に複製します。

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

        }
    )
}

理想的な構文

しかし、私たちはまだ関数を正しい方法で書きたいと思っています。正気を保ちながら、CPS を利用するコードをどのように書くのが理想的でしょうか? 文献には数多くの解釈があります (たとえば、Scalashiftreset演算子は、そうするための多くの方法の 1 つにすぎません) が、正気を保つために、通常の CPS のシンタックス シュガーを作成する方法を見つけてみましょう。それを行うには、いくつかの方法があります。

 

// "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);

...なる...

  • コールバックがチェーン内にある場合、ネーミングを気にせずに簡単に次のコールバックにフィードできます。これらの関数には引数が 1 つしかありません: コールバック引数です。(そうでない場合は、最後の行で次のように関数をカリー化できます。) ここで、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);
  • コールバックが依存関係ツリーを形成する場合、jQuery スタイルのドット チェーンを使用することもできますが、これはネストされた関数を平坦化するという CPS のモナド構文を作成する目的を無効にします。したがって、ここでは詳しく説明しません。

  • コールバックが依存非循環グラフを形成する場合 (たとえば、2*x+3*xx が 2 回使用される場合) 、一部のコールバックの中間結果に名前を付ける方法が必要になります。ここが興味深いところです。私たちの目標は、 http://en.wikibooks.org/wiki/Haskell/Continuation_passing_styledoの構文を、CPS の内外で機能を「アンラップ」および「リラップ」する -notationで模倣しようとすることです。残念ながら、この[1, thrice,[_x]]構文は、私たちが簡単に到達できる最も近いものでした (そして近づくことすらできませんでした)。別の言語でコーディングして JavaScript にコンパイルするか、eval (不吉な音楽をキューに入れる) を使用することができます。ちょっとやり過ぎ。代替では、次のような文字列を使用する必要があります。

 

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

これは、私が説明したものを少し調整するだけで実行できますgenerateComputation'leftPart'最初に、数値ではなく論理名 ( など) を使用するように調整します。次に、関数を実際に次のように動作する遅延オブジェクトにします。

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

(手動ではなく、ある種のデコレータを使用して自動化された方法でこれを行います。)

補足: すべてのコールバック関数は、コールバック パラメータの場所について同じプロトコルに従う必要があります。たとえば、関数がで始まるmyFunction(callback, arg0, arg1, ...)myFunction(arg0, arg1, ..., callback)、互換性がない可能性がありますが、おそらくそうでない場合は、javascript リフレクション ハックを実行して関数のソース コードを調べて正規表現することができるため、心配する必要はありません。それ。

なぜそんなに面倒なことをするのですか?これにより、「インデント地獄」に苦しむことなく、 setTimeouts とs および ajax リクエストを混在させることができます。prompt他にもたくさんの利点があります (10 行の非決定論的検索数独ソルバーを記述できる、任意の制御フロー演算子を実装できるなど)。ここでは説明しません。

于 2012-02-24T23:43:04.337 に答える