延续和回调之间有什么区别? [英] What's the difference between a continuation and a callback?

查看:127
本文介绍了延续和回调之间有什么区别?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我一直在浏览整个网络,寻找关于延续的启示,并且令人难以理解的是,最简单的解释如何能够如此完全混淆像我这样的JavaScript程序员。当大多数文章用Scheme中的代码解释延续或使用monad时,尤其如此。



现在我终于认为我已经理解了延续的本质我想知道是否我所知道的实际上是事实。如果我认为真实并非如此,那就是无知而不是启蒙。



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



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



  var sum = add(2,3); console。 log(sum); function add(x,y){return x + y;}  



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



  add(2,3,function(sum){console.log(sum);}); function add(x,y,cont){cont (x + y);}  



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



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

解决方案

我认为延续是回调的特例。函数可以任意次数回调任意数量的函数。例如:



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



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



  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; //这是forEach的最后一件事// cont是forEach cont(0)的延续; function cont(index){if(index< length){callback(array [index],array,index); //这是cont最后做的事//连续是cont(++ index)的延续; } <} code> 



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



奖金 :继续传递风格。考虑以下程序:



  console.log(pythagoras(3,4)) );函数pythagoras(x,y){return x * x + y * y;}  



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



  console.log(pythagoras(3,4)); function pythagoras(x,y){return add(square(x),square(y))函数square(x){return multiply(x,x);} function multiply(x,y){return x * y;} function add(x,y){return x + y;}  



此外,如果我们不被允许退货任何值,然后我们将不得不使用如下连续:



  pythagoras(3 ,4,console.log);函数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);}  



这种编程风格你不被允许返回值(因此你必须诉诸于传递延续)被称为延续传递风格。



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


  1. 传递延续会增加调用堆栈的大小。除非你使用类似Scheme的语言来消除尾部调用,否则你将面临堆栈空间不足的风险。

  2. 编写嵌套函数很痛苦。

通过异步调用continuation,可以在JavaScript中轻松解决第一个问题。通过异步调用continuation,函数在调用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);}  



第二个问题通常使用名为<$ c的函数来解决$ c> call-with-current-continuation ,通常缩写为 callcc 。不幸的是, callcc 无法在JavaScript中完全实现,但我们可以为其大多数用例编写替换函数:



< pre class =snippet-code-js lang-js prettyprint-override> 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);}函数callcc(f){var cc = function(x){cc = x; }; F(CC); return cc;}



callcc function接受函数 f 并将其应用于 current-continuation (缩写为 CC )。 current-continuation 是一个延续函数,它在调用 callcc 后包装函数体的其余部分。 / p>

考虑函数的主体 pythagoras

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

第二个当前继续 code> callcc 是:

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

同样当前继续第一个 callcc 是:

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

由于当前继续第一个 callcc 包含另一个 callcc 它必须转换为继续传递样式:

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

所以基本上 callcc 逻辑上将整个函数体转换回我们的开始(并给这些匿名函数命名为 cc )。然后使用callcc的这个实现的毕达哥拉斯函数成为:

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

再次无法实现 callcc ,但您可以在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);}  



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


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.

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?

解决方案

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

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.

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:

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-continuation which is often abbreviated as callcc. Unfortunately callcc can't be fully implemented in JavaScript, but we could write a replacement function for most of its use cases:

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 callcc function takes a function f and applies it to the current-continuation (abbreviated as cc). The current-continuation is a continuation function which wraps up the rest of the function body after the call to callcc.

Consider the body of the function 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-continuation of the second callcc is:

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

Similarly the current-continuation of the first callcc is:

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

Since the current-continuation of the first callcc contains another callcc it must be converted to continuation passing style:

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

So essentially callcc logically 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:

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 callcc in JavaScript, but you can implement it the continuation passing style in JavaScript as follows:

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 callcc can be used to implement complex control flow structures such as try-catch blocks, coroutines, generators, fibers, etc.

这篇关于延续和回调之间有什么区别?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆