継続と、それらがコールバック スパゲッティを引き起こしている理由
コールバックを記述すると、「継続渡しスタイル」(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 を実装したため、ハックを使用する必要があります。eval
Javascript では、文字列を作成してそれを 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 を利用するコードをどのように書くのが理想的でしょうか? 文献には数多くの解釈があります (たとえば、Scalashift
とreset
演算子は、そうするための多くの方法の 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*x
x が 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 リフレクション ハックを実行して関数のソース コードを調べて正規表現することができるため、心配する必要はありません。それ。
なぜそんなに面倒なことをするのですか?これにより、「インデント地獄」に苦しむことなく、 setTimeout
s とs および ajax リクエストを混在させることができます。prompt
他にもたくさんの利点があります (10 行の非決定論的検索数独ソルバーを記述できる、任意の制御フロー演算子を実装できるなど)。ここでは説明しません。