延续,以及为什么它们会导致回调意大利面
在回调中编写会迫使您有时采用类似于“连续传递风格”(CPS)的方式进行编写,这是一种非常强大但困难的技术。它代表了控制的完全反转,实际上是“颠倒”了计算。 CPS 使代码的结构明确地反映程序的控制流(有时是好事,有时是坏事)。实际上,您显式地写下了匿名函数的堆栈。
作为理解此答案的先决条件,您可能会发现这很有用:
http://matt.might.net/articles/by-example-continuation-passing-style/ 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
正如您所抱怨的,这会导致代码相当缩进,因为闭包是捕获此堆栈的自然方法。
Monad 来救援
取消缩进 CPS 的一种方法是像 Haskell 中那样“单子”编写。我们该怎么做呢?在 javascript 中实现 monad 的一种好方法是使用点链表示法,类似于 jQuery。 (看http://importantshock.wordpress.com/2009/01/18/jquery-is-a-monad/ http://importantshock.wordpress.com/2009/01/18/jquery-is-a-monad/一个有趣的消遣。)或者我们可以使用反射。
但首先我们需要一种方法来“写下管道”,然后我们可以找到一种方法将其抽象出来。不幸的是,用 javascript 编写通用的 monad 语法有点困难,所以我将使用列表来表示计算。
// 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
这有点难看。但我们可以使这个未缩进的“代码”起作用。我们可以稍后再考虑让它变得更漂亮(在最后一节)。在这里我们的目的是写下所有“必要的信息”。我们想要一种简单的方法来编写每一“行”,以及我们可以编写它们的上下文。
现在我们实现一个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)
, and reduceRight
(由于设计考虑不佳)实际上相当于[1.2.3].reversed().reduce(...)
Above, generateComputation
制作了一堆嵌套函数,在准备过程中将它们彼此包装在一起,并在使用时进行评估...(alert)
,将它们一一剥开,输入到计算中。
旁注:我们必须使用 hack,因为在前面的示例中,我们使用闭包和变量名来实现 CPS。 Javascript 不允许足够的反射来做到这一点,而不求助于制作字符串和eval
所以我们暂时避开函数式风格,选择改变一个跟踪所有参数的对象。因此,上面的内容更接近地复制了以下内容:
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.
)
}
)
}
)
}
)
}
理想语法
但我们仍然希望以合理的方式编写函数。我们将如何ideally喜欢编写我们的代码来利用 CPS,但同时保持我们的理智?文献中有很多观点(例如,Scala 的shift
and reset
运算符只是实现此目的的众多方法之一),但为了理智起见,让我们找到一种为常规 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);
...变成...
- 如果回调位于一个链中,我们可以轻松地将一个回调传递给下一个回调,而不必担心命名。这些函数只有一个参数:回调参数。 (如果没有,您可以在最后一行对函数进行柯里化,如下所示。)这里我们可以使用 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 使用了两次)我们需要一种方法命名中间结果一些回调。这就是有趣的地方。我们的目标是尝试模仿语法http://en.wikibooks.org/wiki/Haskell/Continuation_passing_style http://en.wikibooks.org/wiki/Haskell/Continuation_passing_style以其do
-将函数“展开”和“重新包装”进出 CPS 的符号。不幸的是[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, ...)
or myFunction(arg0, arg1, ..., callback)
它们可能不太兼容,但如果它们不兼容,您可能可以执行 javascript 反射 hack 来查看函数的源代码并将其正则表达式出来,因此不必担心它。
为什么要经历这么多麻烦?这可以让你混入setTimeout
s and prompt
s 和 ajax 请求,而不会遭受“缩进地狱”的困扰。您还可以获得一大堆其他好处(例如能够编写 10 行非确定性搜索数独求解器,以及实现任意控制流运算符),我在此不再赘述。