What's the difference between a continuation and a callback?
Asked Answered
I

3

157

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?

Inexertion answered 24/12, 2012 at 9:12 Comment(16)
A part of me thinks this is a really good question and a part of me thinks it's too long and probably just results in a 'yes/no' answer. However because of the effort and research involved I'm going with my first feeling.Bushey
What's your question? Sounds like you understand this quite well.Athanasius
I'd like to ask: is AJAX a form of "continuation"?Helpless
@MichaelAaronSafyan - This should probably have been a blog post. I was just confused after reading all the explanations about continuations on the web. So I just wanted to know whether what I understand is true.Inexertion
@AaditMShah Makes me think that this should be in a form of Q&A. Though I don't actually know what "continuation" is.Helpless
Yes I agree - I think it probably should have been a blog post more along the lines of 'JavaScript Continuations - what I understand them to be'.Bushey
@AlvinWong - AJAX is AJAX. You can write a function that makes an AJAX call and then throws the result into a continuation, in which case the function is said to be written in continuation passing style.Inexertion
@AndrasZoltan - I finally have something to blog about. Now I need a blog.Inexertion
So now shall I flag this question "Not a real question"?Helpless
Well, there is an essential question: "So what's the difference between a continuation and a callback?", followed by an "I believe...". The answer to that question may be interesting?Overshine
@AaditMShah, I'd recommend that you could split this question into a question 'difference between a callback and continuation' for example and put the rest of the question in an answer, I think that would improve this question and enable others to post other possible answers.Luthanen
This seems like it might be more appropriately posted on programmers.stackexchange.com.Holp
I found the Wikipedia definition excellent: "In computer science and computer programming, a continuation is an abstract representation of the control state of a computer program. A continuation reifies the program control state, i.e. the continuation is a data structure that represents the computational process at a given point in the process' execution; the created data structure can be accessed by the programming language, instead of being hidden in the runtime environment". The rest of the article further clarifies the concept.Gawk
@AaditMShah added a few comments about the terminology in your answer.Invoice
There is a great post about callCC in javascript with stepping debugger in your browser. You can step through and see how control jumps over the code. Details of callCC implementation are provided in the continuation (post).Rudich
Mind that there are bounded an unbounded continuations. Usually a continuation means an unbounded one. A bounded continuation is just a function. About the unbounded one, as far as i have understood, once it is called, no code outside of it matters anymore, and the program terminates after the unbounded continuation terminated.Dacoity
I
186

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.

Inexertion answered 24/12, 2012 at 14:2 Comment(13)
I'm so grateful words cannot describe. I finally understood at intuition level all continuation-related concepts in one sweep! I new once it clicked, it was going to be simple and i would see i used the pattern many times before unknowingly, and it was just like that. Thanks so much for the wonderful and clear explanation.Carboxylate
Unfortunately trampolines require generators.Inexertion
Trampolines are fairly simple, yet powerful stuff. Please check Reginald Braithwaite's post about them.Misfeasance
Thanks for the answer. I wonder if you could possibly provide more support for the statement that callcc can't be implemented in JavaScript? Possibly an explanation of what JavaScript would need to implement it?Mandymandych
@JohnHenry - well, actually there is a call/cc implementation in JavaScript done by Matt Might (matt.might.net/articles/by-example-continuation-passing-style - go to the very last paragraph), but please don't ask me how it works nor how to use it :-)Misfeasance
@JohnHenry JS would need first class continuations (think of them as a mechanism to capture certain states of the call stack). But it has only First Class functions and closures, thus CPS is the only way to mimic continuations. In Scheme conts are implicit and part of callcc's job is to "reify" these implicit conts, so that the consuming function has access to them. That's why callcc in Scheme expects a function as the only argument. The CPS version of callcc in JS differs, because the cont is passed as an explicit func argument. So Aadit's callcc is sufficient for a lot of applications.Mayflower
Since ES2015 supports TCO, CPS makes lot more sense in JS. Wouldn't it be possible to transform an arbitrary, curried function into its CPS version, using a simple combinator? const continuable = f => x => y => cont => cont(f(x)(y));Mayflower
@IvenMarquardt Proper tail calls are still not supported in any major browser. Using a simple combinator to lift functions into the continuation monad is not sufficient. The body of each function would also have to be written in continuation passing style. That would be too tedious to do for every single function. If you really want to use continuations in JavaScript then look at the JavaScript With Advanced Continuation Support program transformer which makes asynchronous programming much simpler.Inexertion
This code is highly amusing, including short yet very hard to mentally evaluate; BUT callcc here doesn't seem to be correct as it returns a timeoutID, not {a normal value: what f is called with} per en.wikipedia.org/wiki/Call-with-current-continuation & https://mcmap.net/q/152758/-call-cc-with-closures#comment-30210361 -why not just define callcc as that last URL does? -is it to fix JS's lack of proper tail calls by using setTimeout? -if so, pls add example(s) which blow the stack w/o, as say recursive factorial. Also hard to see this ‘callcc can be used to implement [big list]’ so pls give examples.Kerley
Amusing code, including the example, pythagoras.async(3, 4, alert), SEEMS it does parallelizing indeed extremely --the goal, yes? But as nothing extra is done in any code following every async() besides returning from function calls, the computation still executes fully sequentially, even for the parallelizable 3^2 and 4^2 which could be simultaneously computed, and (2) it just takes excessively small steps due to use of an async call most everywhere --why so many? -why not just at the parallelizable parts? Overall, if parallelizing is the goal, it seems like a better example is in order, yes?Kerley
@DestinyArchitect My asynchronous callcc function doesn't return a timeout id. It returns undefined. However, the return value does not really matter because the code is asynchronous. The result is passed to the callback function cc.Inexertion
@DestinyArchitect No, pythagoras.async(3, 4, alert) does not run anything in parallel. Everything is sequential. You couldn't make it parallel even if you wanted to because JavaScript is inherently sequential. The only way to run computations in parallel in JavaScript is to use Web Workers (a.k.a. separate threads). Finally, this is just a didactic example. Hence, I made everything asynchronous.Inexertion
@DestinyArchitect The callcc implementation that you linked to is the way callcc is defined for the continuation monad in Haskell. You could do something similar in JavaScript as well but it wouldn't look very different than my example because JavaScript doesn't have Haskell's do notation. Hence, you'll be stuck in callback hell anyway.Inexertion
I
32

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

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 this point 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 callback is 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.

Invoice answered 19/9, 2013 at 10:42 Comment(3)
Thank you for your clarification. You're correct. A continuation is actually a reification of the control state of the program: a snapshot of the state of the program at a certain point in time. The fact that it can be called like a normal function is irrelevant. Continuations are not actually functions. Callbacks on the other hand are actually functions. That's the real difference between continuations and callbacks. Nevertheless JS doesn't support first-class continuations. Only first-class functions. Hence continuations written in CPS in JS are simply functions. Thank you for your input. =)Inexertion
@AaditMShah yes, I misspoke there. A continuation need not be a function (or procedure as I called it). By definition it is simply the abstract representation of things yet to come. However, even in Scheme a continuation is invoked like a procedure and passed around as one. Hmm.. this raises the equally interesting question of what a continuation looks like that is not a function/procedure.Invoice
@AaditMShah interesting enough that I've continued the discussion here: programmers.stackexchange.com/questions/212057/…Invoice
C
18

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

Now, if we pass it a callback:

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

then we will see three alerts: "before", "5" and "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).

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.

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 Lisp languages, just returning the value of the last expression in the function body) but did have call/cc, we could implement return like this:

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 i at which target was found in 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.

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.

Cleanly answered 24/10, 2016 at 22:22 Comment(5)
Apologies if I have made any errors in the JavaScript examples. Writing this answer has roughly doubled the total amount of JavaScript I've written.Cleanly
Do I understand correctly that you are talking about undelimited continuations in this answer, and the accepted answer talks about delimited continuations?Cheju
"invoking a continuation causes execution to resume at the point the continuation was created" -- i think you are confusing "creating" a continuation with capturing the current continuation.Dacoity
@Alexey: this is the kind of of pedantry I approve of. But most languages don't provide any way to create a (reified) continuation other than by capturing the current continuation.Cleanly
@jozef: I am certainly talking about undelimited continuations. I think that was Aadit's intention as well, though as dcow notes the accepted answer fails to distinguish continuations from (closely related) tail calls, and I note that a delimited continuation is equivalent to a fuction/procedure anyway: community.schemewiki.org/?composable-continuations-tutorialCleanly

© 2022 - 2024 — McMap. All rights reserved.