Live data from Hacker News

What Does "With Continuation" Mean? (2020)

forum.snap.berkeley.edu

51–60 of 60 posts

Re: What Does "With Continuation" Mean? (2020)

#51
post #4

I'm glad that this explanation involves a comparison with goto especially with a discussion of "goto considered harmful". But IMO excessively using continuations results in the same kind of spaghetti code as code that excessively uses goto. Delimited continuations, on the other hand, essentially places a restriction on where the continuation can return to. The analogy with using goto is that the target of the goto ha…

Does any language other than Common Lisp provide a "delimited goto"?

For many languages, "delimited goto" is approximated by the language's exception mechanism. You can raise an exception pretty much anywhere you want including in totally different functions many stack frames deep, and the control transfers to the closest catch handler.

Personally I'm not a fan because even the name "exception" carries some baggage. Many people think it should only be reserved for exceptional situations, not expected control flow, and some languages have penalties in performance when using them. That's why they tend not be used for vanilla control flow.

Re: What Does "With Continuation" Mean? (2020)

#52

So there’s a funny thing this doesn’t touch on: the semantics of call/cc is genuinely confusing to understand! There’s a related construct that’s much more legible and has a much easier to understand: call with delimited continuation! Oleg K wrote a very articulate piece about this some long time ago https://okmij.org/ftp/continuations/against-callcc.html

Delimited continuations are in fact harder to understand, because resumed delimited continuations hit an arbitrarily set brick wall, at which point they return like functions: the value which bubbles out of that brick wall is the return value. Thus they don't represent the true future of the computation. The true future of the computation will not hit a prompt and then stop dead, returning a value to the point where it was resumed. It's like a future ... on a yo-yo string. This is an extra feature. By adding code around full blown continuations, we can get delimited ones and so to understand that we have to understand full blown continuations, and that extra code.

Once you get it, you get it. You then understand that it's better for the continuation not to continue into an indefinite future, and easier to reason about when you know that it's going to stop at a certain enclosing contour, and you will have access to the value bubbling out of there.

Once you already understand it, it's very easy to explain it to yourself.

Re: What Does "With Continuation" Mean? (2020)

#53
yield based on delimited continuations in TXR Lisp, showing that unwind-protect works:

  (defun grandkid ()
    (unwind-protect
      (yield-from parent 'in-grandkid)
      (put-line "returning from grandkid")))
 
  (defun kid ()
    (unwind-protect
      (progn
        (yield-from parent 'in-kid)
        (grandkid))
      (put-line "returning from kid")))

  (defun parent ()
    (unwind-protect
      (progn
        (yield-from parent 'in-parent)
        (kid))
      (put-line "returning from parent")))

  (let ((fn (obtain (parent))))
    (prinl 'a)
    (prinl (call fn))
    (prinl 'b)
    (prinl (call fn))
    (prinl 'c)
    (prinl (call fn))
    (prinl 'd)
    (prinl (call fn))
    (prinl 'e))

Run:

  $ txr cont.tl 
  a
  in-parent
  b
  in-kid
  c
  in-grandkid
  d
  returning from grandkid
  returning from kid
  returning from parent
  nil
  e
Each yield captures a new delimited continuation up to the parent prompt. Each (call fn) dispatches a new continuation to continue on to the next yield, or termination.

So with all this back-and-forth re-entry, why do the unwind-protects just go off once? Because of a mechanism that I call "absconding": the answer to the dynamic wind problem.

Absconding is a way of performing a non-local dynamic control transfer without triggering unwinding. It's much like the way, say, the C longjmp is unaware of C++ destructors: the longjmp absconds past them.

With absconding we can get out of the scope where we have resumed a continuation without disturbing the scoped resources which that context needs. Then with the continuation we captured, we can go back there. Everything dynamic is intact. Dynamically scoped variables, established exception handlers, you name it.

The regular function returns are not absconding so they trigger the unwind-protect in the normal way.

absconding is an elephant gun, that should only be used in the implementation of primitives like obtain/yield.

15 second tutorial:

Cleanup yes:

  1> (block foo (unwind-protect (return-from foo 42) (prinl 'cleanup)))
  cleanup
  42
Cleanup no:

  2> (block foo (unwind-protect (sys:abscond-from foo 42) (prinl 'cleanup)))
  42
That's all there is to absconding. yield-from uses it. It captures a new continuation, packages it up and absconds to the prompt, where that is unpacked, the new continuation updated in place of the old so that fn will next dispatch that new one.

Re: What Does "With Continuation" Mean? (2020)

#54

The easiest way to think about continuations is to consider them a generalization of function returns. The continuation of a C function f() is the return address and the saved frame pointer of the calling function -- and that looks a lot like a closure, and that's because it is, except that a) you can only pass that closure one argument in C: the return value, and b) you actually can't get a value for this closure in…

Henry Baker showed that call/cc doesn't require a spaghetti stack with dynamically allocated frames. All you need is one linear stack, and never return.

Richard Stallman made similar observations in Phantom Stacks. If You Look to Hard They Aren't There.

Chicken Scheme, based on C, implements Baker's idea. Chicken Scheme's C functions never return. They take a continuation argument, and just call that instead of returning. So every logical function return is actually a new C funtion call. These all happen on the same stack, so it grows and grows. All allocations are off the stack, including lambda environments, and other objects like dynamic strings. When the stack reaches a certain limit, it is rewound back to the top, like a treadmill. During this rewinding phase, all objects allocated from the stack which are still reachable are moved to the heap.

Thus the combination of the CPS strategy (all returns are continuation invocations) and the linear stack with rewinding obviates the need for dynamically allocated frames.

Re: What Does "With Continuation" Mean? (2020)

#55
post #4

I'm glad that this explanation involves a comparison with goto especially with a discussion of "goto considered harmful". But IMO excessively using continuations results in the same kind of spaghetti code as code that excessively uses goto. Delimited continuations, on the other hand, essentially places a restriction on where the continuation can return to. The analogy with using goto is that the target of the goto ha…

Simply using simple tail calls is goto-like spaghetti code.

Any if/goto program graph or flowchart can be turned into tail calls that have the same shape. For each node in the goto graph, we can have a tail-called function, 1:1.

Re: What Does "With Continuation" Mean? (2020)

#56

Earlier quoted context omitted.

Continuations are closures. Closures aren't continuations. Though one can build continuations out of closures by converting code into continuation passing style, which makes continuations explicit, and then `call/cc` is trivial, since all it does is pass (to its function argument) its [now-explicit, after CPS conversion] continuation, thus reifying it.

Continuations are just closures in cps. Closures are just functions plus an environment parameter. Functions are just gotos plus a link pointer. Yet each abstraction is more than the sum of its components.

Continuations are closures because they capture everything that a closure would capture at the same point in the execution. But they also capture more.

In CPS, continuations are often closures. But: those closures also close over the surrounding function's hidden continuation parameter k, and make essential use of it! The last continuation-lambda in the function has to call k to simulate the return.

Re: What Does "With Continuation" Mean? (2020)

#57

The easiest way to think about continuations is to consider them a generalization of function returns. The continuation of a C function f() is the return address and the saved frame pointer of the calling function -- and that looks a lot like a closure, and that's because it is, except that a) you can only pass that closure one argument in C: the return value, and b) you actually can't get a value for this closure in…

Simpler still is to recognise that "call a function" and "return from a function" are different syntax over the same thing. They both mean "jump to somewhere with a convention about where to find state". If you replace "call a function" with goto, and replace "return from a function" with goto, then it becomes immediately obvious that "continuation" is a name for where you're going to jump to next. It only looks comp…

> like four registers available for passing arguments and one available for returning a result, when the calling convention really should be symmetric

The symmetry implies the support for multiple return values.

If the language model has single return values, then continuations take one parameter. Lots of historic papers about continuations model them that way.

Multiple values are tricky. 100% symmetry is never achieved with those things. The problem is that in many contexts, an expression is expected to produce one value. We usually want (foo (bar) (baz)) to call foo with two arguments even if bar and baz return two or more values. There may be times when we want to inerpolate all the values, or some of them, into the argument space, so we need some syntax to distinguish those situations. But if (foo (bar) (baz)) just takes one value from each function, then that means that the primary value is more of a first class citizen than the additional values. There is something special about it.

We can also go the other way: declare that functions should not only return exactly one value, but only take exactly one argument. That is also symmetric! Then currying can be used to combine functions in order to simulate multiple arguments.

Re: What Does "With Continuation" Mean? (2020)

#58

The easiest way to think about continuations is to consider them a generalization of function returns. The continuation of a C function f() is the return address and the saved frame pointer of the calling function -- and that looks a lot like a closure, and that's because it is, except that a) you can only pass that closure one argument in C: the return value, and b) you actually can't get a value for this closure in…

Henry Baker showed that call/cc doesn't require a spaghetti stack with dynamically allocated frames. All you need is one linear stack, and never return. Richard Stallman made similar observations in Phantom Stacks. If You Look to Hard They Aren't There. Chicken Scheme, based on C, implements Baker's idea. Chicken Scheme's C functions never return. They take a continuation argument, and just call that instead of retur…

I'm aware of this work, especially Chicken Scheme, but Chicken scheme basically combines the stack and the heap + compacting GC, so it's a reach to say that Chicken Scheme doesn't allocate frames on the heap... If you have to GC frames, then they are as-if on the heap. Same thing for related variants.

You can also just allocate all frames on the stack and use stack copying for `call/cc` -- this involves... a heap of stack copies :laugh: so I think I'm henceforth going to say that `call/cc` continuations always require heap allocations :)

Re: What Does "With Continuation" Mean? (2020)

#59

Earlier quoted context omitted.

Simpler still is to recognise that "call a function" and "return from a function" are different syntax over the same thing. They both mean "jump to somewhere with a convention about where to find state". If you replace "call a function" with goto, and replace "return from a function" with goto, then it becomes immediately obvious that "continuation" is a name for where you're going to jump to next. It only looks comp…

> like four registers available for passing arguments and one available for returning a result, when the calling convention really should be symmetric The symmetry implies the support for multiple return values. If the language model has single return values, then continuations take one parameter. Lots of historic papers about continuations model them that way. Multiple values are tricky. 100% symmetry is never achie…

> The symmetry implies the support for multiple return values.

Yes!

> Multiple values are tricky. 100% symmetry is never achieved with those things. The problem is that [...]

You basically need destructuring on the return values, and the syntax for that has to be fairly clean. And for the example you give where one function's return value(s) is(are) passed to another's things do get tricky: shall that use only the first value returned? or maybe the whole lot of them (as an array/list)? or both, where if the callee doesn't treat the thing as an array/list then it's the first value (implying dynamic typing)? or does what happens depend on the callee's parameter's type? or what? Yeah, it's very tricky.

Another option is to have generators, and if a function returns/yields multiple values in one go, maybe treat them as single values yielded separately. This one isn't that awesome either.

> We can also go the other way: declare that functions should not only return exactly one value, but only take exactly one argument. That is also symmetric! Then currying can be used to combine functions in order to simulate multiple arguments.

Yes :)

But then in Haskell, which does this, we end up with syntactic sugar with which to pretend there's multiple arguments, thus the 100% symmetry isn't quite.

Oh well.

Re: What Does "With Continuation" Mean? (2020)

#60

Earlier quoted context omitted.

Simpler still is to recognise that "call a function" and "return from a function" are different syntax over the same thing. They both mean "jump to somewhere with a convention about where to find state". If you replace "call a function" with goto, and replace "return from a function" with goto, then it becomes immediately obvious that "continuation" is a name for where you're going to jump to next. It only looks comp…

> like four registers available for passing arguments and one available for returning a result, when the calling convention really should be symmetric The symmetry implies the support for multiple return values. If the language model has single return values, then continuations take one parameter. Lots of historic papers about continuations model them that way. Multiple values are tricky. 100% symmetry is never achie…

This is tricky in the comment format but here goes.

Let us declare that a function is passed exactly one value, and that it passes exactly one value to the chosen continuation. However that does not imply that it takes exactly one named argument.

(lambda a ...)

Binds that one value to a. If you pass it a list, that's a variadic function.

(lambda (a b) ...)

Requires the argument be a list of two values and binds the elements to a, b.

(lambda (a (b c) d) ...)

Likewise, except it's now a list of three things where element 1 must be a list of two things.

The corresponding return/continuation part then looks like

(let a (foo) (b c) (bar) ...)

where the let binds whatever foo returned to a, binds a list of length 2 from bar and so forth.

This works really nicely with static typing in the absence of function currying. Destructuring bind on function arguments is somewhat common.

In lisp, you have to work out what let returns when the binding doesn't typecheck at runtime and that's a bit of a mess.

In SML the construct typechecks at compile time but I can't work out how to reconcile it with currying.

Parameter trees, the (a (b) c) idea, I first saw in kernel. I don't remember if it went as far as let binding / destructuring on the continuation invocation.

I like the destructuring syntax more than currying so haven't put as much thought into providing both as the latter might deserve.

Post reply on HN