
Unwind-protect vs. Continuations (2003) - pera
http://www.nhplace.com/kent/PFAQ/unwind-protect-vs-continuations-original.html
======
bjoli
Unwind protect doesn't work when you have multi-shot continuations. The
question is whether you should have multi-shot continuations. I would say yes
(but delimited since they are easier to reason about and superior in every
way).

We could bolt on a simpler exit facility on top of call/cc with support for
proper unwind protect, but it would not bring much compared to a simple macro
hiding the ugly parts of dynamic wind (and of course: making sure to only use
continuations once... ).

------
orthecreedence
I wrote cl-async. I _really_ missed continuations because any synchronous code
that wants to use async basically has to be rewritten all the way up to accept
async. This can be somewhat alleviated with promises, but it's still a rewrite
no matter how you slice it.

Continuations would have solved this completely by allowing first-class green
threads or coroutines or whatever the hell you want to call them. There's cl-
cont, which is a horribly hacky syntax macro that converts all your code to
CPS and, as I remember it, kind of sidesteps proper error handling altogether.

So, you can argue over which is better for error handling all you want, but I
would have strangled a dolphin to have continuations/coroutines back when I
was building out the async ecosystem stuff.

------
discardable_dan
While I don't have a definitive formulation in mind, this seems like exactly
the sort of problem that can be solved with delimited continuations by
'augmenting' any outer continuation with a code to perform the file-pointer
closing.

That said, I'm not certain demanding these operations use `unwind-protect` is
desirable. Several dialects of Scheme use continuations for multithreading,
and forcing the program to close a file handle before context-switching also
seems like incorrect behavior: if I ever end up back in the file-reading
context, it sure would be nice if that file was still open. The author
proposes that we should indicate _if we intend to do this_ in _each
continuation case_ (either at `call/cc` or continuation invocation time),
which seems annoying and cumbersome.

The alternative, then, which the author seems to ignore, is to trust the
garbage collector to do the correct thing. Luckily, any BiBoP mark-and-sweep
collector for Scheme [0] will almost certainly handle this situation when the
file handle is out of scope (by correctly closing handles during the
compacting step).

0\.
[http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.51.4...](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.51.4490)

------
dietrichepp
Call/cc is powerful and simple in the sense that goto is powerful and simple.
It’s possible to implement other control structures with call/cc or goto, but
when you combine them it becomes difficult to reason about the program.

Many years ago I was working on my own Scheme implementation. This kind of
project is mostly easy, but I stopped working on it as soon as I started
implementing mapcar. I just didn’t want to deal with it, and all the dirty
effects of call/cc.

~~~
convolvatron
maybe you insisted on using a call stack? if you don't care about the
difficult to quantify performance delta just use heap allocated activation
records - it all falls out pretty easily.

the one thing I would never want to implement is hygenic macros, but if you
steal a reference version most of the special forms get boiled away.

~~~
dietrichepp
The problem I’m referring to has nothing to do with the call stack, it’s
actually a problem of semantics. “It all falls out pretty easily” does not
match my experiences at all. Heap allocated, garbage collected activation
records were my expectation going in.

If the function passed to call/cc returns multiple times, you have to return a
full result multiple times. This means something like constructing a reversed
list of the results and returning a reversed copy of that list.

It was at that point I realized I had no desire to work with a language that
had call/cc, and I was sorely disappointed when the Scheme committee decided
not to publish a Scheme subset without it.

Call/cc is like dynamite. Powerful, but all the precautions you have to go
through in order to use it safely negate the benefits.

~~~
convolvatron
oh wait, did you evert the expressions (turn inside out) so that each function
gets passed a continuation (a function to call with the return value)?

then each of the multiple returns is just another call to this implicit
continuation.

just saying - not trying to defend RNRS, I think they kinda wandered off into
the wilderness. its also kind of sad that high level language designs get
wrapped around these kinds of implementation issues when ostensibly they are
abstractions well above the fray.

~~~
dietrichepp
Not really familiar with the terminology here—I don’t know what it means to
“evert” an expression. Using CPS or something else doesn’t matter here because
this is not a compiler issue, this is a problem with the library that stems
from the language definition itself.

Calling a continuation multiple times works as expected if the continuation is
pure. For example, consider the following map implementation for one list:

    
    
        (define (map f x)
          (if (null? x)
              ()
              (cons (f (car x))
                    (map f (cdr x)))))
    

This works but creates a number of active activation records equal to the list
length, which is not ideal. Attempts to optimize this are confounded by the
fact that f may return multiple times—not that it’s impossible to optimize,
just that multiple returns make optimization more difficult.

Then consider the case for, e.g., vector-map, where the most obvious
implementation does not handle multiple returns correctly in the first place,
even before you optimize it. This is why I became so disinterested in Scheme.

~~~
pflanze
Why didn't you conclude instead that first-class (or just multi-shot)
continuations are still an experimental feature in Scheme, and that the
important part is to warn users of that fact ("use at your own risk, because
.. undefined ..")?

Have you moved to Common Lisp instead?

Edit: I agree that call/cc is pretty nasty; but at least as a debugging (and
sometimes hacking) tool it can also be very useful. As a user you can always
refrain from using it in regular code. Or decide to use it anyway and suffer
from non-portability. So in a sense, the Scheme subset without call/cc is
already there, just refrain from using call/cc or any library that uses it?
(To be fair, this might be one of the reasons that impedes the growth of
portable libraries, but I haven't followed the latest developments of the
standardization efforts, and whether things around it are better defined now.)

Edit 2: I should also note that there are Scheme implementations that
implement activation records more efficiently (in some form that's closer to
an array based stack, e.g. segmented stacks or a sort of delayed segmentation
approach that Gambit takes), in which case the direct recursive implementation
of map that you have shown is more efficient than iteration and then reversal.
(And aside of being the straight-forward way to write map, this variant is
also easily made into a streaming variant by just adding one |delay| into it
(and a |force| for the list argument if |car| and |cdr| don't do that).) One
thing I haven't pondered is how call/cc interacts with deforestation like
implemented in Haskell, I have to go and experiment with that.

~~~
dietrichepp
The R7RS standard _explicitly states_ that map must be able to handle multiple
returns. So saying “use at your own risk” is kind of like saying, “this is my
own Scheme dialect with the following differences…”

Common Lisp is basically a melange of implementation quirks from the 1980s
frozen in time. Even though I’ve personally used it more than Scheme, it’s a
mess to implement.

------
kazinator
In TXR Lisp, I came up with a way to get (delimited) continuations to play
along with unwind-protect and exceptions.

The key is to regard continuations as time slices of green threads. Now in a
thread, we (of course!) do not dispose of the local resources just because we
have context switched to another thread!

So I decided to try the same thing with continuations, and it works fairly
well.

I augmented the language run-time with a non-unwinding dynamic escape
operator. I call the non-unwinding form of escape "absconding". The operator
is _abscond-from_ , contrasting with _return-from_.

Using absconding, we can yield a continuation out of some contour without
bailing the local resources, and then use the continuation to come back in.
(This is not unlike a context switch out of and back to a thread.)

All the normal call and return mechanisms in the code that we are suspending
and resuming with continuations perform normal unwinding.

[http://nongnu.org/txr/txr-manpage.html#N-01C4E6B4](http://nongnu.org/txr/txr-
manpage.html#N-01C4E6B4)

It's a Solved Problem, as far as I'm concerned.

