javascript 延续和回调有什么区别?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/14019341/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-26 20:30:05  来源:igfitidea点击:

What's the difference between a continuation and a callback?

javascriptcontinuationscallcccontinuation-passing

提问by Aadit M Shah

I've been browsing all over the web in search of enlightenment about continuations, and it's mind boggling how the simplest of explanations can so utterly confound a JavaScript programmer like myself. This is especially true when most articles explain continuations with code in Scheme or use monads.

我一直在浏览整个网络以寻找有关延续的启迪,令人难以置信的是,最简单的解释如何让像我这样的 JavaScript 程序员如此彻底地困惑。当大多数文章用 Scheme 中的代码解释延续或使用 monad 时,尤其如此。

Now that I finally think I've understood the essence of continuations I wanted to know whether what I do know is actually the truth. If what I think is true is not actually true, then it's ignorance and not enlightenment.

现在我终于认为我已经理解了延续的本质,我想知道我所知道的是否真的是事实。如果我认为正确的事情实际上并不正确,那么这是无知而不是开悟。

So, here's what I know:

所以,这就是我所知道的:

In almost all languages functions explicitly return values (and control) to their caller. For example:

在几乎所有语言中,函数都将值(和控制)显式地返回给它们的调用者。例如:

var sum = add(2, 3);

console.log(sum);

function add(x, y) {
    return x + y;
}

Now in a language with first class functions we may pass the control and return value to a callback instead of explicitly returning to the caller:

现在在具有一流函数的语言中,我们可以将控制和返回值传递给回调,而不是显式返回给调用者:

add(2, 3, function (sum) {
    console.log(sum);
});

function add(x, y, cont) {
    cont(x + y);
}

Thus instead of returning a value from a function we are continuing with another function. Therefore this function is called a continuation of the first.

因此,我们将继续使用另一个函数,而不是从函数中返回值。因此这个函数被称为第一个的延续。

So what's the difference between a continuation and a callback?

那么延续和回调有什么区别呢?

回答by Aadit M Shah

I believe that continuations are a special case of callbacks. A function may callback any number of functions, any number of times. For example:

我相信延续是回调的特例。一个函数可以回调任意数量的函数,任意次数。例如:

var array = [1, 2, 3];

forEach(array, function (element, array, index) {
    array[index] = 2 * element;
});

console.log(array);

function forEach(array, callback) {
    var length = array.length;
    for (var i = 0; i < length; i++)
        callback(array[i], array, i);
}

However if a function calls back another function as the last thing it does then the second function is called a continuation of the first. For example:

然而,如果一个函数回调另一个函数作为它所做的最后一件事,那么第二个函数被称为第一个函数的延续。例如:

var array = [1, 2, 3];

forEach(array, function (element, array, index) {
    array[index] = 2 * element;
});

console.log(array);

function forEach(array, callback) {
    var length = array.length;

    // This is the last thing forEach does
    // cont is a continuation of forEach
    cont(0);

    function cont(index) {
        if (index < length) {
            callback(array[index], array, index);
            // This is the last thing cont does
            // cont is a continuation of itself
            cont(++index);
        }
    }
}

If a function calls another function as the last thing it does then it's called a tail call. Some languages like Scheme perform tail call optimizations. This means that the tail call does not incur the full overhead of a function call. Instead it's implemented as a simple goto (with the stack frame of the calling function replaced by the stack frame of the tail call).

如果一个函数调用另一个函数作为它所做的最后一件事,那么它被称为尾调用。Scheme 等一些语言执行尾调用优化。这意味着尾调用不会产生函数调用的全部开销。相反,它被实现为一个简单的 goto(调用函数的堆栈帧被尾调用的堆栈帧替换)。

Bonus: Proceeding to continuation passing style. Consider the following program:

奖励:继续传球风格。考虑以下程序:

console.log(pythagoras(3, 4));

function pythagoras(x, y) {
    return x * x + y * y;
}

Now if every operation (including addition, multiplication, etc.) were written in the form of functions then we would have:

现在,如果每个操作(包括加法、乘法等)都以函数的形式编写,那么我们将有:

console.log(pythagoras(3, 4));

function pythagoras(x, y) {
    return add(square(x), square(y));
}

function square(x) {
    return multiply(x, x);
}

function multiply(x, y) {
    return x * y;
}

function add(x, y) {
    return x + y;
}

In addition if we weren't allowed to return any values then we would have to use continuations as follows:

此外,如果我们不允许返回任何值,那么我们将不得不使用延续如下:

pythagoras(3, 4, console.log);

function pythagoras(x, y, cont) {
    square(x, function (x_squared) {
        square(y, function (y_squared) {
            add(x_squared, y_squared, cont);
        });
    });
}

function square(x, cont) {
    multiply(x, x, cont);
}

function multiply(x, y, cont) {
    cont(x * y);
}

function add(x, y, cont) {
    cont(x + y);
}

This style of programming in which you are not allowed to return values (and hence you must resort to passing continuations around) is called continuation passing style.

这种不允许返回值的编程风格(因此您必须求助于传递延续)称为延续传递风格。

There are however two problems with continuation passing style:

然而,延续传递风格有两个问题:

  1. Passing around continuations increases the size of the call stack. Unless you're using a language like Scheme which eliminates tail calls you'll risk running out of stack space.
  2. It's a pain to write nested functions.
  1. 传递延续会增加调用堆栈的大小。除非您使用像 Scheme 这样消除尾调用的语言,否则您将面临耗尽堆栈空间的风险。
  2. 编写嵌套函数很痛苦。

The first problem can be easily solved in JavaScript by calling continuations asynchronously. By calling the continuation asynchronously the function returns before the continuation is called. Hence the call stack size doesn't increase:

第一个问题可以在 JavaScript 中通过异步调用 continuation 轻松解决。通过异步调用延续,函数在调用延续之前返回。因此调用堆栈大小不会增加:

Function.prototype.async = async;

pythagoras.async(3, 4, console.log);

function pythagoras(x, y, cont) {
    square.async(x, function (x_squared) {
        square.async(y, function (y_squared) {
            add.async(x_squared, y_squared, cont);
        });
    });
}

function square(x, cont) {
    multiply.async(x, x, cont);
}

function multiply(x, y, cont) {
    cont.async(x * y);
}

function add(x, y, cont) {
    cont.async(x + y);
}

function async() {
    setTimeout.bind(null, this, 0).apply(null, arguments);
}

The second problem is usually solved using a function called call-with-current-continuationwhich is often abbreviated as callcc. Unfortunately callcccan't be fully implemented in JavaScript, but we could write a replacement function for most of its use cases:

第二个问题通常使用称为函数的函数来解决,该函数call-with-current-continuation通常缩写为callcc。不幸的是callcc不能在 JavaScript 中完全实现,但我们可以为它的大多数用例编写一个替换函数:

pythagoras(3, 4, console.log);

function pythagoras(x, y, cont) {
    var x_squared = callcc(square.bind(null, x));
    var y_squared = callcc(square.bind(null, y));
    add(x_squared, y_squared, cont);
}

function square(x, cont) {
    multiply(x, x, cont);
}

function multiply(x, y, cont) {
    cont(x * y);
}

function add(x, y, cont) {
    cont(x + y);
}

function callcc(f) {
    var cc = function (x) {
        cc = x;
    };

    f(cc);

    return cc;
}

The callccfunction takes a function fand applies it to the current-continuation(abbreviated as cc). The current-continuationis a continuation function which wraps up the rest of the function body after the call to callcc.

callcc函数采用一个函数f并将其应用于current-continuation(缩写为cc)。该current-continuation是延续函数调用后包起来的函数体的其余部分callcc

Consider the body of the function pythagoras:

考虑函数体pythagoras

var x_squared = callcc(square.bind(null, x));
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);

The current-continuationof the second callccis:

所述current-continuation第二的callcc是:

function cc(y_squared) {
    add(x_squared, y_squared, cont);
}

Similarly the current-continuationof the first callccis:

类似地current-continuation,第一个callcc是:

function cc(x_squared) {
    var y_squared = callcc(square.bind(null, y));
    add(x_squared, y_squared, cont);
}

Since the current-continuationof the first callcccontains another callccit must be converted to continuation passing style:

由于current-continuation第一个callcc包含另一个callcc它必须转换为连续传递样式:

function cc(x_squared) {
    square(y, function cc(y_squared) {
        add(x_squared, y_squared, cont);
    });
}

So essentially callcclogically converts the entire function body back to what we started from (and gives those anonymous functions the name cc). The pythagoras function using this implementation of callcc becomes then:

所以本质callcc上是将整个函数体从逻辑上转换回我们开始的地方(并为这些匿名函数命名为cc)。使用 callcc 实现的毕达哥拉斯函数变为:

function pythagoras(x, y, cont) {
    callcc(function(cc) {
        square(x, function (x_squared) {
            square(y, function (y_squared) {
                add(x_squared, y_squared, cont);
            });
        });
    });
}

Again you can't implement callccin JavaScript, but you can implement it the continuation passing style in JavaScript as follows:

同样,您无法callcc在 JavaScript 中实现,但您可以在 JavaScript 中实现延续传递样式,如下所示:

Function.prototype.async = async;

pythagoras.async(3, 4, console.log);

function pythagoras(x, y, cont) {
    callcc.async(square.bind(null, x), function cc(x_squared) {
        callcc.async(square.bind(null, y), function cc(y_squared) {
            add.async(x_squared, y_squared, cont);
        });
    });
}

function square(x, cont) {
    multiply.async(x, x, cont);
}

function multiply(x, y, cont) {
    cont.async(x * y);
}

function add(x, y, cont) {
    cont.async(x + y);
}

function async() {
    setTimeout.bind(null, this, 0).apply(null, arguments);
}

function callcc(f, cc) {
    f.async(cc);
}

The function callcccan be used to implement complex control flow structures such as try-catch blocks, coroutines, generators, fibers, etc.

该函数callcc可用于实现复杂的控制流结构,例如 try-catch 块、协程、生成器、纤维等。

回答by dcow

Despite the wonderful writeup, I think you're confusing your terminology a bit. For example, you are correct that a tail call happens when the call is the last thing a function needs to execute, but in relation to continuations, a tail call means the function does not modify the continuation that it is called with, only that it updates the value passed to the continuation (if it desires). This is why converting a tail recursive function to CPS is so easy (you just add the continuation as a parameter and call the continuation on the result).

尽管写得很精彩,但我认为你有点混淆了你的术语。例如,当调用是函数需要执行的最后一件事时,会发生尾调用是正确的,但是对于延续,尾调用意味着该函数不会修改被调用的延续,只是它更新传递给延续的值(如果需要)。这就是将尾递归函数转换为 CPS 如此简单的原因(您只需添加延续作为参数并在结果上调用延续)。

It's also a bit odd to call continuations a special case of callbacks. I can see how they are easily grouped together, but continuations didn't arise from the need to distinguish from a callback. A continuation actually represents the instructions remaining to complete a computation, or the remainder of the computation from thispoint in time. You can think of a continuation as a hole that needs to be filled in. If I can capture a program's current continuation, then I can go back to exactly how the program was when I captured the continuation. (That sure makes debuggers easier to write.)

将延续称为回调的特殊情况也有点奇怪。我可以看到它们是如何很容易地组合在一起的,但是延续并不是因为需要与回调区分开来。延续实际上表示完成计算的剩余指令,或从时间点开始计算的剩余部分。您可以将延续视为需要填补的漏洞。如果我可以捕获程序的当前延续,那么我就可以准确地回到我捕获延续时程序的状态。(这确实使调试器更容易编写。)

In this context, the answer to your question is that a callbackis a generic thing that gets called at any point in time specified by some contract provided by the caller [of the callback]. A callback can have as many arguments as it wants and be structured in any way it wants. A continuation, then, is necessarily a one argument procedure that resolves the value passed into it. A continuation must be applied to a single value and the application must happen at the end. When a continuation finishes executing the expression is complete, and, depending on the semantics of the language, side effects may or may not have been generated.

在这种情况下,您的问题的答案是,回调是一种通用的东西,在调用者[回调]提供的某个合同指定的任何时间点都会被调用。回调可以有任意数量的参数,并且可以按照它想要的任何方式构造。因此,continuation必然是一个解析传递给它的值的单参数过程。必须将延续应用于单个值,并且应用程序必须发生在最后。当延续完成执行时,表达式就完成了,并且根据语言的语义,可能会或可能不会产生副作用。

回答by cpcallen

The short answer is that the difference between a continuation and a callback is that after a callback is invoked (and has finished) execution resumes at the point it was invoked, while invoking a continuation causes execution to resume at the point the continuation was created. In other words: a continuation never returns.

简短的回答是延续和回调之间的区别在于,在调用回调(并完成)后,执行会在它被调用的点恢复,而调用延续会导致执行在创建延续的点恢复。换句话说:延续永远不会返回

Consider the function:

考虑函数:

function add(x, y, c) {
    alert("before");
    c(x+y);
    alert("after");
}

(I use Javascript syntax even though Javascript doesn't actually support first-class continuations because this was what you gave your examples in, and it will be more comprehensible to people not familiar with Lisp syntax.)

(我使用 Javascript 语法,尽管 Javascript 实际上并不支持一流的延续,因为这是您给出示例的内容,对于不熟悉 Lisp 语法的人来说,它会更容易理解。)

Now, if we pass it a callback:

现在,如果我们传递一个回调函数:

add(2, 3, function (sum) {
    alert(sum);
});

then we will see three alerts: "before", "5" and "after".

然后我们将看到三个警报:“before”、“5”和“after”。

On the other hand, if we were to pass it a continuation that does the same thing as the callback does, like this:

另一方面,如果我们向它传递一个与回调做同样事情的延续,就像这样:

alert(callcc(function(cc) {
    add(2, 3, cc);
}));

then we would see just two alerts: "before" and "5". Invoking c()inside add()ends the execution of add()and causes callcc()to return; the value returned by callcc()was the valued passed as the argument to c(namely, the sum).

那么我们只会看到两个警报:“before”和“5”。调用c()insideadd()结束执行add()并导致callcc()返回;返回的值callcc()是作为参数传递给的值c(即总和)。

In this sense, even though invoking a continuation looks like a function call, it is in some ways more akin to a return statement or throwing an exception.

从这个意义上说,尽管调用延续看起来像一个函数调用,但它在某些方面更类似于 return 语句或抛出异常。

In fact, call/cc can be used to add return statements to languages that don't support them. For example, if JavaScript didn't have return statement (instead, like many Lips languages, just returning the value of the last expression in the function body) but did have call/cc, we could implement return like this:

事实上, call/cc 可用于向不支持它们的语言添加 return 语句。例如,如果 JavaScript 没有 return 语句(相反,像许多 Lips 语言一样,只是返回函数体中最后一个表达式的值)但有 call/cc,我们可以像这样实现 return:

function find(myArray, target) {
    callcc(function(return) {
        var i;
        for (i = 0; i < myArray.length; i += 1) {
            if(myArray[i] === target) {
                return(i);
            }
        }
        return(undefined); // Not found.
    });
}

Calling return(i)invokes a continuation that terminates the execution of the anonymous function and causes callcc()to return the index iat which targetwas found in myArray.

调用return(i)调用终止匿名函数的执行并导致callcc()返回itarget中找到的索引的延续myArray

(N.B.: there are some ways in which the "return" analogy is a bit simplistic. For example, if a continuation escapes from the function it was created in - by being saved in a global somewhere, say - it is possible that the function that created the continuation can return multiple times even though it was only invoked once.)

(注意:在某些方面,“返回”类比有点简单化。例如,如果延续从创建它的函数中逃逸 - 通过保存在全局某处,比如说 - 函数可能创建延续的对象可以多次返回,即使它只被调用一次。)

Call/cc can similarly be used to implement exception handling (throw and try/catch), loops, and many other contol structures.

Call/cc 可以类似地用于实现异常处理(throw 和 try/catch)、循环和许多其他控制结构。

To clear up some possible misapprehensions:

澄清一些可能的误解:

  • Tail call optimisation is not by any means required in order to support first-class continuations. Consider that even the C language has a (restricted) form of continuations in the form of setjmp(), which creates a continuation, and longjmp(), which invokes one!

    • On the other hand, if you naively try to write your program in continuation passing style without tail call optimisation you are doomed to eventually overflow the stack.
  • There is no particular reason a continuation need take only one argument. It's just that argument(s) to the continuation become the return value(s) of call/cc, and call/cc is typically defined as having a single return value, so naturally the continuation must take exactly one. In languages with support for multiple return values (like Common Lisp, Go, or indeed Scheme) it would be entirely possible have continuations that accept multiple values.

  • 为了支持一流的延续,绝不需要尾调用优化。考虑一下,即使是 C 语言也有一种(受限的)形式的延续,形式为setjmp(),它创建了一个延续,而longjmp(),它调用了一个!

    • 另一方面,如果您天真地尝试在没有尾调用优化的情况下以连续传递风格编写程序,您注定最终会溢出堆栈。
  • 没有什么特别的理由让一个延续只需要一个参数。只是延续的参数变成了 call/cc 的返回值,并且 call/cc 通常被定义为具有单个返回值,因此延续自然必须恰好是一个。在支持多个返回值的语言中(如 Common Lisp、Go 或实际上是 Scheme),完全有可能有接受多个值的延续。