Hacker News new | past | comments | ask | show | jobs | submit login
Web Programming with Continuations (2002) [pdf] (thelackthereof.org)
56 points by dmux 13 days ago | hide | past | web | favorite | 35 comments

One of the Racket tutorials actually goes into an example of using continuations to save application state for a web app (https://docs.racket-lang.org/more/index.html#%28part._.Conti...).

One issue with this approach is that it doesn't really scale easily beyond a single server. You need something like the JVM which allows for closures to be sent between machines sharing the same code base. Plus, even in the JVM case, you must have the class files required by the closure available on the JVM which tries to run it.

I really think it is a better idea to just use a database and pass state explicitly rather than trying to do something fancy with continuations. However, continuations can be extremely useful for making the server asynchronous without making it look asynchronous.

While Promises are nice, I think they are severely flawed in that they must "infect" any code which tries to use them. For instance, I was dealing with some Typescript compiler interfaces a few years back which expected to be able to read files synchronously, something which does not hold true if you try to emulate a filesystem in the browser using web requests. With true continuations, it would be possible to simply pause this computation transparently and then resume it once the request was complete.

I don't think you need to pass closures between machines but you do need to use "sticky sessions" so that your load balancers deliver all requests for a particular session back to the same app instance.

I haven't seen much discussion of sticky sessions in quite a while as 12-factor apps built around stateless processes have no need for them and that has been the dominant paradigm for quite a while.

An application requiring sticky session sounds like an architecture smell to me. It works, and you can get a long way with it, but a system that relies on requests always being served by the same server sounds a bad idea to me. What if that server breaks/need a security update/has to be rebooted? For security updates & restarts you can always use session draining, but then your operativity becomes dependent on an upstream service (a LB?), which in a large organization is probably handled by another team, and you end up needing to coordinate and complicate something that could have been simple.

As long as it's a conscious choice, it's ok not baking in tolerance for these failure modes, but personally I would only accept it for POCs/prototypes.

These are serious concerns. An attempt to make web programming with continuations work without requiring sticky sessions is "Stateless Servlets":


A stateless servlets makes it possible to serialize continuations and store them on the client/database/disk.

There is no such thing as a free lunch, so that approach has pros and cons too.

Yeah, I don't see how this type of architecture pays off in the end. You're opening yourself up to long-running continuations that provide a DoS memory attack vector, put pressure on the GC, and you still have to figure out when to actually GC. Imagine a shopping cart continuation hanging out for days or even weeks, waiting on the user to click the order button. Now imagine thousands of these.

And for what? Just so you can run some logic on the server? Which you really shouldn't be doing in the first place. And who is the lucky person that has to deal with server affinity? Seems like the developer is just tossing his work over the fence to some poor dev ops guy to clean up.

Actually, Racket does have serializable closures.


I prefer to explicitly serialize state, because its easier to reason about. Its harder to assert that request A was definitely using/not-using some bit of state that request B was using when you are automatically serializing a bunch of lexical variables in the background.

This is just the bias of my personal experience though, I'm a pretty low-level guy.

Speaking of serializable closures: does anyone here know of any work being done on languages that would support nameable, completely size-transparent and versionable closures across different code versions?

This isn’t something out of the ordinary, you would think - but I can easily see how that could be hard to implement.

It seems passing closure is not quite scalable since any closure could be accidentally extremely huge.

I found for distribute environment, the big idea is still messaging.

The racket web framework has an option to serialize the entire continuation for he client to send back to you. Pretty intense.

A framework that works with continuations is Seaside in smalltalk, it is a delight to work with it. https://seaside.st

That HTTPS link is broken for me, PR_CONNECT_RESET_ERROR in Firefox. http://seaside.st works.

Wrote my bachelor thesis with it in 2009 and it was awesome. You'd be surprised how similiar it had been to react in a way.

As mentioned in the document, continuation passing style can at times be a little difficult to read. That being said I found a talk that using continuation passing style to provide typed routing. [1]

This won't be the most efficient way to perform routing in an application, but i found the implementation really simple to understand and the error messages for the end-user seem really clear as well. It also helps if the language used allows for embedding DSLs that are easy to read for the end-user. (could be macros in lisp/elixir, well named functions with a little sprinkling of infix operators in OCaml, Haskell etc)

A little show HN in the comments: I've been exploring this approach and others [2] for comparison, and although i've moved away from this style as I progress in learning more OCaml, it was a fun little exercise to explore a CPS based typed routing approach. [3]

[1] https://dbp.io/talks/2016/fn-continuations-haskell-meetup.pd...

[2] https://github.com/anuragsoni/routes

[3] https://github.com/anuragsoni/routes/blob/0.1.0/src/routes.m...

> As mentioned in the document, continuation passing style can at times be a little difficult to read.

Very true. Fortunately, the CPS transform is automatic, so one could write a compiler which compiles non-CPS to CPS code.

This is pretty easy in Scheme or Lisp (less so the latter, because it's a much more complex & powerful language); it's not nearly easy in other languages, but still doable.

With support for first class (preferably delimited) continuations this is not a problem. I have used scheme and racket to write small web seevices, and it is really an amazing tool to have.

I never had to make it scale though, but it is an amazing experience.

Around the same period, in the telephony world, IVR (interactive voice response) systems entered the web universe with the VoiceXML W3C recommendation. It allowed IVR developers to write server-side web applications as ordinary web applications, the HTML being substituted by VoiceXML.

Given the very linear nature of an IVR dialog, it became clear that a continuation-based approach would greatly simplify development, as mentioned in the article.

The Nu Echo "Rivr" Java framework was created around this idea. However, since there is no native continuation in Java (yet!), it was approximated through thread-based coroutines.

The payoff for the developers is huge. It allows the developer to directly "map" a dialog specification to Java code. Abstractions can easily be introduced without having to create an explicit model for the state (the state is just the set of the variables and objects referred by the thread). This approach is much more flexible than state-machine based paradigms.

Even though all architectural issues mentioned here are valid (namely the lack of session-based fault-tolerance), it turned out to be somewhat manageable in real-life situations because in this type of system, there is a limited number of calls handled simultaneously and their average duration is typically a few minutes.

Rivr is still used today and we are currently defining mechanisms with HAProxy to implement no-downtime scenarios in Kubernetes.

Also from 2002 I believe, an example of a project that was doing this with JavaScript using Rhino, the at-the-time state of the art Java implementation of the language that supported capturing a continuation.


It should be noted that the owner/author of this website (https://thelackthereof.org/ and not the posted PDF) wrote a Perl continuations based web library/framework called Continuity.

- https://thelackthereof.org/Continuity

- https://metacpan.org/pod/Continuity

Shame it is no longer under development. Found it very handy & enjoyable using it for projects in the past (by itself and sometimes with Squatting framework built on top of it). Hats off to Brock (https://news.ycombinator.com/threads?id=awwaiid) for the great work creating Continuity.

Many of the comments here seem worried that scalability issues are going to crop up everywhere as people start using this architecture in large, popular applications. That isn't going to happen. Stop worrying about that and enjoy it for the cool hack that it is.

Paul Grahams mentions Closures and continuation passing style in Viaweb here: http://ep.yimg.com/ty/cdn/paulgraham/bbnexcerpts.txt

I am new to this, but isn't the idea of calling next() which is prevalent in frameworks like express.js, essentially similar to continuations? I may be wrong, but I would genuinely like to know.

Yes. It’s easy when you can run it all client-side. The tricky part described in the article is making it work across no-JS browser and webserver.

Isn’t this how HN is implemented?

I know that it was in the past, but I believe it changed a few years ago.

See: https://news.ycombinator.com/item?id=7651568

It used to be, until they realized that generating a unique link for the 2nd page for every viewer didn't exactly scale.

EDIT: Also the fnid issue that EvanAnderson mentioned.

Please don't design web applications like this. It's counter to the core ideas of HTTP and REST. It's just about as bad a design as is ASP shoveling all its state into POST requests.

Not to mention that you have to choose between either dealing with garbage-collecting continuations stored on the server (choosing between either breaking client bookmarks, or bloating the server with never-to-be-used junk), or worrying about the security implications of storing continuations on the client (remembering that a continuation is arbitrary code which will executed on your server).

And since you don't have a well-defined HTTP API, but rather one directly coupled to the structure of your source code, all those links will break every time you change your code.

This design was notably promulgated by the Racket (then Dr. Scheme) crowd, who were (and I assume still are) notoriously allergic to state and so used mechanisms like this to try to hide it.

Please don't try to hide state. Especially in truly stateful systems like HTTP and user interfaces. Embrace state. Minimize it, isolate it, document it, control it, model it, but embrace it.

> or worrying about the security implications of storing continuations on the client (remembering that a continuation is arbitrary code which will executed on your server).

You could apply encryption (with proper randomization to deal with possible replay attacks, if necessary e.g. if you keep state on the server).

> Embrace state. Minimize it, isolate it, document it, control it, model it, but embrace it.

But continuations/closures are a way to control state. It's just that the state is not accessible from the outside of the system (the user).

> It's counter to the core ideas of HTTP and REST.

No, it isn't really. If you are sending continuations along with a request, then the server can still behave in a purely functional way.

> You could apply encryption

Or you could identify which bits of state actually matter and define a stable REST API, instead of identifying application state as "any state the interpreter could possibly be in" in an attempt to pretend that state doesn't exist.

> But continuations/closures are a way to control state.

No, they're a way to hide state. But state is integral to the experience of a web application. Hiding it is just a leaky abstraction (for the reasons pointed out above).

> the server can still behave in a purely functional way

You're going to have to explain how that makes this technique RESTful.

Notably, if you store the continuation on the server, you're violating REST's statelessness requirement: https://en.wikipedia.org/wiki/Representational_state_transfe...

Rolling up all the system's state into a single URI whose underlying state cannot be modified violates the requirements that each URI represent a distinct resource which can be manipulated (i.e. changed) using standard HTTP verbs: https://en.wikipedia.org/wiki/Representational_state_transfe...

Once you have persistent data (say, flight details stored in a database), you lose cacheability, since each interaction with that data from a different source is via a different URI: https://en.wikipedia.org/wiki/Representational_state_transfe...

At least this technique is pure HATEOAS, so there's that: https://en.wikipedia.org/wiki/HATEOAS

I don't disagree that there are places within a web application where this technique makes sense. The one example of this technique which is always presented – a short-lived dialogue-based interaction with the user – is one of them. That particular state is sufficiently short-lived, and doesn't need to be cacheable or shareable with others, that pretending it doesn't exist is probably helpful in the long run.

But I posit that in general, it's always a better architectural decision to explicitly identify your state (i.e., everything which you're keeping in a database or on disk) and minimize it.

> But state is integral to the experience of a web application

HTTP is a stateless protocol and the desire to maintain a state-full session on the server is why web applications are fundamentally broken, at least within the context of HTTP.

Right, which is why session state should be kept on the client, and why the client should manipulate application state (i.e., everything that lives in a database) using a well-defined RESTful interface.

The approach in the OP seems to conflate these two types of state. The end result is application state without a well-defined client interface, and session state which is passed backed and forth to the server.

> The approach in the OP seems to conflate these two types of state.

Nope, I'm just saying that continuations and REST are orthogonal.

> in an attempt to pretend that state doesn't exist.

I think you misunderstand continuations, as they are closures which often do contain state. In fact, they can be used to capture all of the state relevant to the session.

I understand continuations perfectly. It is exactly the "capturing all of the state relevant to the session" which is problematic. Implicitly grabbing your application's state – including the context of what code is executing, and all in-scope variable bindings, which is the only "state" in a purely functional server such as you surmised earlier – and wrapping it into an opaque blob to pass to a client is exactly what I mean by "hiding state". Failing to identify and define exactly what is the relevant state with which clients should interact is exactly what leads to issues with upgrades, scalability, and security.

I'd have no problem with this technique if, instead of an arbitrary call/cc or what-have-you, the developer had to explicitly name the continuation, and specify and name which bits of state are relevant. This definition could then form the basis of a well-defined public-facing interface, for which application tooling could ensure smooth upgrades, for which security properties could be easily demonstrated, and for which more stringent persistence rules than heuristic-based garbage collection could be defined.

this is what serialisable continuations[0] provide.

[0] https://docs.racket-lang.org/web-server/stateless.html#(part...

> Please don't design web applications like this. It's counter to the core ideas of HTTP and REST.

It's not, as long as the server state is designated by a URL. The client-side continuation is perfectly compatible with REST, and it's safety of client-side continuations is preserved with crypto.

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