
Writing a Lisp: Continuations - reinvdwoerd
http://reinvanderwoerd.nl/blog/2017/05/16/writing-a-lisp-continuations
======
kpil
Continuations are strangely underused, as they enables writing long-living
processes in a simple way, without having to keep them in running in a thread,
or even in memory.

Then a real programming language can replace all "business processing" crap
languages. Let's say you write a framework that escapes to a continuation
whenever the "process" is waiting for Futures or Promises to complete, and
returns the thread to a pool.

Depending on what you are waiting for, such as webhooks, asynch
events/messages, or with some work even outstanding network requests ( if it's
meaningful ), it would be possible to serialize the continuation to storage,
potentially restore it on another machine, weeks later.

With some bookkeeping, it would be possible to also store what Futures have
been completed, and what the continuation is currently waiting for, so it
would be possible to keep track of the progress of the long running process,
and use that information to show a nice status report.

~~~
brandonbloom
I have implemented interpreters that support both full and delimited
continuations. They are underused for many very good reasons. A continuation
makes an implicit state machine. Implicit means anonymous, which means harder
to talk about, which means harder to reason about. But there are technical
problems too.

Continuation based sessions, as used in fringe web frameworks such as Seaside
or even Arc's library that powers Hacker News, is a really problematic
approach. Reifying a slice of call stack in to a continuation and serializing
it introduces all sorts of problems for resource management and the user
experience, especially in the presence of change.

For example, let's say you deploy a new version of your code: What happens to
all the in-progress sessions? Do you let old code keep running? Do you force
users to start over? Compare to "Edit and Continue" in a Java/C#/Smalltalk
codebase where you can change a method and all instances get the new behavior,
vs if you change a function that constructs another function: All the old
closures stick around with the old code.

Let's say a user leaves a browser tab open for a while: How long do you wait
before invalidating the session and free its resources? What impact does that
have on the user experience? Hacker News has mostly eliminated its reliance on
continuations to remedy these sorts of problems.

Speaking of resources: What resources does a continuation hold on to? If you
serialize a continuation, do you keep all file handles open? Do you perform
static analysis to know a priori what data should be garbage collected or
excluded from the continuation as "semantic garbage"? This problem is double
bad when laziness is involved. Consider paginating through a database cursor.

What happens when a continuation must be forked? Consider using a
continuation-id to resume an interaction with a web app: Does opening a new
tab duplicate any held resources by the continuation? Do your external
resources even support cloning operations or are they thread safe? Compare to
unix "fork" and file handles vs shared memory.

~~~
ajuc
> For example, let's say you deploy a new version of your code: What happens
> to all the in-progress sessions?

This is a problem inherent to long-running processes, no matter the
implementation. We had the same problem in our business process engine using
jbpm (implemented as an interpreter serializing state to database every step).

Ultimately you have to decide if you bind
functions/subroutines/subprocesses/whatever you call them early (so when you
call them later and there's a new version - they still use old version), or
late - so they are always calling the newest version. And you have to adjust
your coding assumptions and the way you update your software to new version
basing on that decision.

Neither way is always the correct one.

~~~
aconz2
Came to say the exact same thing, all the problems mentioned aren't unique to
continuations. I'm not saying this makes a positive argument to use coroutines
everywhere, but it's not their fault.

As ajuc mentioned, long-running processes and early/late bound is always a
thing. Same with database migrations. Heck, even clients accessing a versioned
api (foo.com/api/v1/bar) vs not has the same consideration.

> How long do you wait before invalidating the session and free its resources?

Same decision has to be made for explicit session data like login state and
pagination cursors in eg. Facebook's graph API. Continuations are larger and
therefore this question may have more weight.

> What resources does a continuation hold on to?

Anything reachable (static and/or dynamic analysis). And if you run your
program in a monad (or other restricted, pure manner), then you can control
when a function can suspend, like not in the middle of paging a database.

> Does opening a new tab duplicate any held resources by the continuation?

If you live in an immutable world, you get duplication for free with
structural sharing. Same goes for an OS loading a shared library once and
giving the same pages to multiple programs.

I do agree that having a blob of stuff serialized isn't very nice. Same goes
for closures as well. The example I always compare in my head is a functional
representation of a set vs. any other data structure.

I toyed around with an interpreter that would let you reflect a closure (same
could be applied to continuations) to a program, which is akin to a residual
program in partial evaluation. Then you could do whatever you want with the
source of the closure/continuation. For the early/late bound question above,
you could analyze the reflected continuation and decide exactly which bits you
want to rebind before continuing the continuation.

------
simplify
I remember trying to learn continuations during my CS degree, and evidently
even today I still don't understand them. The examples don't seem to help
either – how exactly does the control flow function?

~~~
kmill
You know how in Python the "yield" statement is actually an expression and can
be sent values using "next"? Imagine there is a special "yield" called "call-
with-current-continuation" that can be used anywhere (not just in generators)
which bundles everything up and makes a generator-like thing called a
"continuation." This continuation object can be called later on, like using
"next" in Python. Unlike generators, though, continuations can usually be
resumed repeatedly, always resuming at the same point where the call-with-
current-continuation expression was.

A weird thing is that call-with-current-continuation doesn't yield _back_ to
something; it yields to the function given to call-with-current-continuation.

~~~
coldtea
Would that be like having a yield statement AND passing a context (e.g.
binding a this that represents "current-continuation")?

~~~
kmill
Here are two examples in Python syntax, assuming "yield" is no longer a
keyword.

    
    
      def f():
        def receiver(yield):
          yield(2)
          # yield jumps out of the 'receiver' function and never returns
          print("This never prints")
      
        x = callcc(receiver)
        return x+1
      
      print(f())
      # f returns 2+1
      
      # backtracks stores (continuation,[values]) pairs.  Whenever a computation fails, we pull out another
      # value from the list and feed it to the continuation.  The idea is to perform a depth-first search.
      backtracks = []
      def fail():
        """Aborts the current continuation and backtracks to another alternative stored in backtracks."""
        if not backtracks: raise Exception('amb ran out of alternatives')
        yield, alternatives = backtracks.pop()
        alt = alternatives.pop()
        if alternatives: # this continuation still has alternatives, so put them back on the list in case of a future 'fail'
          backtracks.push((yield, alternatives))
        yield(alt)
      def amb(*alternatives):
        """The "ambivalent operator."  The arguments to amb are alternatives, and the return value of amb
        is an alternative which makes the future computation not fail."""
        
        if not alternatives: fail()
        def receiver(yield):
          backtracks.append((yield, alternatives))
          fail()
        return callcc(receiver)
      def expect(b):
        if not b: fail()
      
      def test_amb():
        """Let's find a Pythagorean triple."""
        x = amb(*range(1,100))
        y = amb(*range(1,100))
        z = amb(*range(1,100))
        expect(x**2 + y**2 == z**2)
        return (x,y,z)
      
      # We can print out all the Pythagorean triples.
      trip = test_amb()
      print("(x,y,z)=(%s,%s,%s)" % trip)
      fail() # fail every time so it keeps backtracking, but the side effect of printing out remains

------
tbodt
My favorite part of this blog post is the Fira Code webfont

------
e40
Are there any continuation implementations in Common Lisp?

~~~
ScottBurson
Yes, cl-cont by our very own Slava "coffeemug" Akhmechet. Unfortunately, a
recent site rebuild at common-lisp.net seems to have left the project page
blank; nor do I see a cl-cont repo on Slava's Github page. It's Quicklisp-
installable, though.

------
pmoriarty
Do any non-Lisp/Scheme languages have continuations?

~~~
rprospero
Here's the obligatory "Haskell has a monad for that" comment

[http://hackage.haskell.org/package/mtl-2.2.1/docs/Control-
Mo...](http://hackage.haskell.org/package/mtl-2.2.1/docs/Control-Monad-
Cont.html)

~~~
penpapersw
That kind of feels like cheating though. I mean, the complicated part of a
continuation is that it restores the same context and state that the app had
right before the plunge. But Haskell inherently doesn't have "floating" state,
it's all self-contained within each parameter. So in Haskell it's more or less
a fancy goto-statement.

~~~
mbrock
The linked module defines a "monad transformer" that can wrap other monads
with a continuation-capturing layer. This means, among other things (Haskell's
solution is awesomely general) that you can indeed use it with state, if you
compose it with the state monad, or even the IO monad.

(The bottom of the documentation page has a contrived example of this.)

