Hacker News new | past | comments | ask | show | jobs | submit login

(Edit: Oops! Sorry, I see that you're already familiar with continuations, but let me explain them for those who aren't)

Sometimes, fundamental switches in thinking are worth it. :)

For example, continuations would be perfect here, and are a wonderful tool for simplifying "nested callback hell" type problems.

But why should a web developer care about continuations?

Consider Paul Graham's Arc challenge: Build a web page that presents a form to the user. After they submit the form, a new page presents them with a link. The link takes them to a third page where the submitted value is presented back to the user. The restriction is that the input field must not be passed in via the URL.

Right now, the user must write in "continuation-passing style", which means threading callbacks, like this:

   function serve(req,res) {
      askUserForValue(req,res, function(value){
          showLink(req, res, function(){
              render(res, {"user-value": value});
          });
      });
   }
Or perhaps the user could somehow stuff the variable into a "session" library with three separate URL routes:

    function serve(req,res) {
       render the form with the POST action of /submit;
    }

    function submit(req,res, value) {
       req.session["uservalue"] = value;
       render the link that sends the user to /view;
    }

    function view(req,res) {
       render the value of req.session["uservaule"];
    }
This one is even more fragmented since you have three separate functions that model one user interaction flow! I certainly like it less than the callback example -- at least I can keep one in my head.

We can do better though. Continuations let you write this:

   function serve(req, res) {
      var value = askUserForValue();
      showLink();
      render(res, {"user-value": value});
   }
If node.js supported continuations, the askUserForValue() function would respond with the form and suspend the serve function right then and there. After the user made a new HTTP call, the suspended askUserForValue() call would return to serve() and its return value would be placed into our variable. Then, the showLink() function could render the link and suspend the function again. Once the user makes another HTTP request, the function resumes, and so on.

The server would have to store the state of the computation itself to disk so it could continue later.

With continuations, we can pretend the user's web browser is the program counter. This frees my mind up. First, I don't have to keep callbacks in my head since there aren't any callbacks anymore. Second, stack traces work the way I expect because functions explicitly call each other. Third, if askForValue() throws an exception if the user inputs a string where I entered a number, no problem! Just wrap it in a try{} block that asks the user to enter a valid value, as if I'm writing a text program that calls readline().

Also note that this is different from just letting each user interaction be in its own thread! Consider:

1. Continuations should be serializable to disk. You wouldn't want blocked threads to just pile up as more users visit your website, and if your server crashes, you certainly wouldn't want to forget their state.

2. The browser "back" button would have to step a program backwards in time; something that is unsupported in a naïve thread-equals-user-interaction system. (E.g. how many of you have booked an airplane ticket with Travelocity/Expedia/whatever, pressed "Back", and then got a big error because the server didn't know how to step backwards through its user stack?) But the ability to save continuations to disk is the ability to save the current state of the program to disk, which makes stepping back in time easy: you can simply reload what was about to happen, just like savestates in a video game emulator.

This is not a new idea. Many Common Lisp and Smalltack web frameworks work well with continuations and have done so for years (the first Seaside release was 2002, I'm not kidding you!)

Racket, a great dialect of Scheme, ships with such a web framework out of the box. Here's my implementation of the Arc challenge in Racket. Note how the 'start' function is implemented: https://gist.github.com/gcr/ad116f565e105c8b2e0d

It's super easy to try this out and you don't need to fiddle with dependencies or anything. Just download Racket from http://racket-lang.org/ , copy+paste the code into DrRacket's giant text box on the top, press Run, and your web browser will pop up with the example.




This is not possible with promises or generators, because their flow is unidirectional (so no back button). It may be possible to come up with an abstraction that works like promises (.then chaining) but can be rewinded, in which case the code would look like this:

  askUserForValue
    .then(value => res.showLink()
      .then(_ => res.render(res, {"user-value": value}))
Infact I think something like this could probably be built with monadic observables and a nice library. The only thing you couldn't do is automagically keep intermediate state anywhere else other than memory :/

The thing is, isn't this just a pipe dream? The moment you need to interact with an external system, like say an SQL database, you can say good-bye to rewinding, or be fine with the fact that you might commit a transaction twice, or otherwise come up with a complex method to also rewind the database state, or write your own database that supports this :)

Edit: Also, these days this approach is largely unnecessary. Now the entire thing would work on the client and the server would only be responsible to provide an API :P


Your last paragraph is why Racket provides ways of invalidating "in-progress" continuations. Ideally, in any user flow, you'd want to have the only side-effect operation occur at the end of the flow (User clicks the 'Book ticket' or 'Process transaction' button); the hard part is usually modeling it beforehand in a way that allows the user to navigate naturally back and forth between the parts of the application/open it up in new tabs or whatever.

But sure, there are limitations of course. :)


I see. It definitely looks powerful. Do you know if this is possible in any non-LISP language?

I mean, invalidating of the "observable" chain could also be done in JS, but I don't think that I know of other languages that could automatically transfer the state to the client.


I'm not sure. I honestly think it's a culture thing. Many Lisp communities embrace continuations, but everyone else rightly shies away because they make your head hurt.

Ruby(!) supports them, so it might be possible for Rails to create something like the pattern I mentioned. In fact, Ruby people use continuations for a few things like the built-in Generator class and making "restartable exceptions" so the thrower can figure out how to deal with the problem rather than the catcher, which is sometimes quite useful: http://chneukirchen.org/blog/archive/2005/03/restartable-exc...

Properly implementing continuations is a huge implementiation burden though, which is another reason why the're uncommon. The language would have to make the stack frame itself slicable and serializable, and it's hard to get good performance when the call stack itself becomes a "first class" language construct. Consider a duplicate() function that saves the state of a program and then spawns ten threads that each reload that state! Have fun implementing continuations in a way that supports that, Guido! :)


Node.js 0.11 supports generators, which appear to be very similar to what you're describing: http://blog.stevensanderson.com/2013/12/21/experiments-with-...


Similar, but less general.

Serializable continuations can be saved to disk. You can reinstate a completed continuation (eg. go back from the final page. After clicking the link again, the page completes again. Or try clicking the link in several tabs; the page concurrently completes several times as you expect.) Generators make this pattern impossible; Continuations give it to you for free.

Second, by being saved on disk, they aren't crammed up in the event loop so they aren't a RAM leak. Racket even provides a way of having the client store all state if you wish; such "stateless" continuations have no memory or disk overhead on a long-running server and never expire until you change your site's source code.


In short: Generators are useful when you use callbacks to wait for the server to finish doing something (eg. finish a DB update or an HTTP request)

Continuations are useful when you're waiting for the user to finish doing something (eg. an HTTP response, not a request).

So they do solve different problems.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: