javascriptpipecontinuation-passing

Is continuation passing style any different to pipes?


I've been learning about continuation passing style, particularly the asynchronous version as implemented in javascript, where a function takes another function as a final argument and creates an asychronous call to it, passing the return value to this second function.

However, I can't quite see how continuation-passing does anything more than recreate pipes (as in unix commandline pipes) or streams:

replace('somestring','somepattern', filter(str, console.log));

vs

echo 'somestring' | replace 'somepattern' | filter | console.log

Except that the piping is much, much cleaner. With piping, it seems obvious that the data is passed on, and simultaneously execution is passed to the receiving program. In fact with piping, I expect the stream of data to be able to continue to pass down the pipe, whereas in CPS I expect a serial process.

It is imaginable, perhaps, that CPS could be extended to continuous piping if a comms object and update method was passed along with the data, rather than a complete handover and return.

Am I missing something? Is CPS different (better?) in some important way?

To be clear, I mean continuation-passing, where one function passes execution to another, not just plain callbacks. CPS appears to imply passing the return value of a function to another function, and then quitting.


Solution

  • UNIX pipes vs async javascript

    There is a big fundamental difference between the way unix pipes behave vs the async CPS code you link to.

    Mainly that the pipe blocks execution until the entire chain is completed whereas your async CPS example will return right after the first async call is made, and will only execute your callback when it is completed. (When the timeout wait is completed, in your example.)

    Take a look at this example. I will use the Fetch API and Promises to demonstrate async behavior instead of setTimeout to make it more realistic. Imagine that the first function f1() is responsible for calling some webservice and parsing the result as a json. This is "piped" into f2() that processes the result.

    CPS style:

    function f2(json){
        //do some parsing
    }
    
    function f1(param, next) {
       return fetch(param).then(response => response.json()).then(json => next(json));
    }
    
    // you call it like this:
    f1("https://service.url", f2);
    

    You can write something that syntactically looks like a pipe if you move call to f2 out of f1, but that will do exactly the same as above:

    function f1(param) {
       return fetch(param).then(response => response.json());
    }
    
    // you call it like this:
    f1("https://service.url").then(f2);
    

    But this still will not block. You cannot do this task using blocking mechanisms in javascript, there is simply no mechanism to block on a Promise. (Well in this case you could use a synchronous XMLHttpRequest, but that's not the point here.)

    CPS vs piping

    The difference between the above two methods is that who has the control to decide whether to call the next step and with exactly what paramters, the caller (later example) or the called function (CPS).

    A good example where CPS comes very handy is middleware. Think about a caching middleware for example in a processing pipeline. Simplified example:

    function cachingMiddleware(request, next){
         if(someCache.containsKey(request.url)){
             return someCache[request.url];
         }
         return next(request);
    }
    

    The middleware executes some logic, checks if the cache is still valid: