[racket-dev] Changing call/cc

From: Asumu Takikawa (asumu at ccs.neu.edu)
Date: Wed Aug 29 23:06:52 EDT 2012

Hi all,

Sam, Stevie, and I have been working on a contract mechanism for control
operators and allowing delimited control to be used in Typed Racket. In
the process, we've found out that `call/cc` combined with delimited
control is incompatible with Typed Racket.

We have a proposal for changing `call/cc` so that it interacts better
with delimited operators and would be safe to include in TR. The
proposal is to remove the current `call/cc` and replace it with a
function implemented with delimited control like the following:

(define (call/cc f tag)
  (call-with-composable-continuation
   (lambda (k)
    (f (lambda vs
        (abort-current-continuation
         tag
         (lambda ()
          (call-with-continuation-prompt
           (lambda ()
            (apply k vs))
           tag
           (lambda (thunk) (thunk))))))))))

(taken from collects/tests/racket/prompt.rktl)

The motivation for this change is that `call/cc` is currently difficult
to locally reason about. In particular, if you set up a continuation
prompt, you would expect that either its body happily returns with no
control effects or the handler is invoked. For example,

;; expect a number here
(+ 1 (call-with-continuation-prompt
       (lambda ()   ; f is a (-> Number) function
         (f))       ; but might have control effects
       prompt-tag
       (lambda (x)  ; called on abort
         5)))

you might expect this code to always return a number if it returns.
Unfortunately, `call/cc` can violate this expectation. In particular, if
`f` calls a continuation captured by `call/cc` elsewhere, then it
will erase the body code and could return, say, a string and cause an
error. The handler is not called on a `call/cc`-based abort.

This is a problem for Typed Racket as well, since if the programmer
can't locally reason about this code, neither can the type system
(without extra static *and* dynamic protection).

  ***

The alternative implementation has two semantic differences that could
potentially be an issue:

  1) The alternative `call/cc` will trigger abort handlers, which means
     an interceding prompt with the right prompt tag could interfere
     with a `call/cc`.

  2) The alternative `call/cc` triggers more `dynamic-wind` handlers than
     the original `call/cc`.

I've looked into whether 1) is actually an issue. This would break code
that both uses `call/cc` and installs a custom abort handler. In
practice, few people do this. Code in the core codebase either does not
use both features together, or work fine with the delimited version.

I've also looked at uses of `call/cc` in the PLaneT sources. There is a
single package (murphy/amb.plt) where I am sure the change will break
functionality, but there's a simple workaround. All other uses were
either traditional Scheme-like uses of `call/cc` or use an unexported
prompt tag with no custom handlers.

Scheme-like uses of `call/cc` are not a problem as long as you don't
install a prompt with a default prompt tag but *not* a default handler.
This is rarely done and only affects the body of that prompt.

2) is an issue only in code that uses `call/cc` and `dynamic-wind`
together. This is also uncommon and there is a good delimited
workaround: install a prompt at the point at which you want
`dynamic-wind` handlers to stop being called.

  ***

Summary: `call/cc` is problematic for local reasoning about programs and
we'd like to replace it with a version implemented with delimited
operators. Its semantics would be slightly different, but this impact
should be limited. In a new language, the right choice is likely to
avoid `call/cc` to begin with [1], but we need to provide backwards
compatibility.

Any thoughts?

[1]: http://okmij.org/ftp/continuations/against-callcc.html

Cheers,
Asumu

Posted on the dev mailing list.