Hacker News new | ask | show | jobs
by JonChesterfield 863 days ago
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 complicated from the context of calls/returns/stacks.

Confusing call and return for different things is unfortunate but popular. It gives rise to things like four registers available for passing arguments and one available for returning a result, when the calling convention really should be symmetric.

"Continuation passing style" is what all function calls use - x86 writes the address of the continuation to the stack, more sensible ISA's pass it in a specific register - modulo language syntax that somewhat obfuscates control flow.

2 comments

> 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".

That's pretty much what I was saying, but pithier and better stated, so thank you.

> 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 complicated from the context of calls/returns/stacks.

Yes, thus "lambda is the ultimate GOTO".

> Confusing call and return for different things is unfortunate but popular. It gives rise to things like four registers available for passing arguments and one available for returning a result, when the calling convention really should be symmetric.

It's a convenient abstraction for Algol family languages.

There is a difference between function call and function return in those languages: a call pushes a frame on the stack, and a return pops a frame off the stack, but both otherwise look very similar under the covers.

In CPS the "pop a frame off the stack" part doesn't happen, but instead you get tail-call optimization (TCO) to avoid the stack blowing up with calls to functions that never return (and which anyways maybe store their real call frames on the heap to boot, thus wasting all that stack space for nothing).

> 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.

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.

> 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.