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

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




I think of `call/cc` as a parlor trick that helps introduce the concept of continuations more generally.

Threads and co-routines are on the heavy-weight end of the concurrent programming techniques spectrum because they require stacks -possibly large, with guard pages and all- and encourage smearing application state onto that stack, increasing cache pressure.

Continuation passing style (CPS) is on the light-weight end because it encourages the programmer to make application state explicit and compact rather than smearing it on a stack. Callback hell is hand-coded continuation passing style (CPS). Async/await is a compromise that gets one close to the light weight of continuations.

To understand all of that one has to understand the concept of continuations. In computer science, the cheap parlor trick that is `call/cc` is helpful in introducing the concept of continuations to students. After all, `call/cc` is shocking when one first sees it, and the curious will want to understand it.


I think now I get why I find Python's async/await semantics (asyncio) so much more convoluted than Javascript's.

Javascript starts with simple callbacks. Those indeed have no stack, only a shallow list of local variables as state. Then async/await is modelled as a relatively straightforward syntactic sugar on top of that: An await call is still just a callback behind the scenes; if one async function awaits another async function, you get something that looks like a stack, but is really just a chain of callbacks.

In contrast, Python starts with coroutines, which do have a stack, then models async/await by surrounding them with a scheduling runtime. Unfortunately, for async code to be useful, you still need support for callbacks, so you end up with both: an await call represents a mix of suspended coroutine stacks and callback chains, which can be much more complicated to reason about.


Continuations (which can be just closures following CPS conversion) can be used to construct co-routines, and so if you start with co-routines instead...

But callbacks and callback-based async/await force you to compress application state better, so you will get better performance out of that. No need for `call/cc` style continuations, just light-weight continuations, but this is mainly a mirage because in the callback model you do in fact have continuations, it's just that the continuation is only ever "the next step in processing this thing" rather than "the whole stack".

That is, with hand-coded CPS you get a very shallow stack to capture in continuations, so the continuations are very cheap.


Approximately everything by Oleg is great!

We half-jokingly considered renaming our Sydney Computer Science Paper Club to 'Oleg Fan Club'.


Hehe fun!

Yeah, delimited continuations have much saner semantics than undelimited ones. Partly because they’re always well defined


I like the notation "limited continuations" more than "undelimited". Drops the double negative and provides a pejorative element.

Call/cc still has a delimiter, it's just some second class thing above the scope of the current program that you can't do much with which thwarts composition. Let the programmer specify where the delimiter is and you get a less limited construct.


"undelimited" is not a double negative. "Delimited" means "limited by an explicit boundary."

Introducing a different concept with the same word "limited" is confusing.

"specifiable continuations" may be a clearer way to make a positive connotation.


Yes. It's also pretty clear how to use delimited continuations in a pure setting without mutation.

I'm not quite sure you can make use of something like call/cc without having at least one mutable variable or side-effect somewhere?


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.


Is there any resource that explains the different varieties of the limited continuations in a way that doesn't require an advanced theoretical CS degree? I see that Racket has both prompt/control and shift/reset but I've never been able to make sense of how they differ, if at all




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

Search: