1
2
Fork
You've already forked cio
0
An effect handlers library for Racket and Guile Scheme, built atop delimited continuations and prompts.
  • Racket 60%
  • Scheme 40%
2025年08月17日 22:53:23 -07:00
benchmarks/cio track implementations for the effect handler benchmark suite 2025年08月17日 22:53:23 -07:00
cio guile: sketches of (cio exn) and (cio iter) 2025年07月26日 20:32:56 -07:00
.gitignore track .gitignore so jujutsu doesn't crash out 2025年08月15日 13:02:01 -07:00
info.rkt initial version 2 implementation atop the prompts system (+ guile) 2025年07月18日 16:28:47 -07:00
LICENSE add more tests 2025年02月21日 22:04:03 -08:00
README.md update README for cio v2 2025年08月15日 13:50:45 -07:00

cio: an effect handlers library atop delimited continuations and prompts

cio is an (untyped) structural effect handlers library for Racket and Guile Scheme.

This was primarily a learning project for me to better understand how effect handlers relate to delimited continuations. If you want to use it, go ahead! The implementation is fairly minimal and (hopefully) fairly comprehensible. Be warned that it is not well-tested (yet), and not guaranteed to behave with other continuation-based libraries (though it should).

(define-effect write)
(define (action)
 (raise-effect write "Hello")
 (raise-effect write "world!"))
(try (action)
 [(catch write msg)
 (println msg)
 (resume)])
(try (action)
 [(catch write msg)
 (println (format "I see you tried to print '~a'. Not so fast!" msg))
 (resume)])
(define-effect read)
(define (another-action)
 (raise-effect write (string-append "Hello, " (raise-effect read))))
(try (another-action)
 [(catch write msg)
 (println msg)
 (resume)]
 [(catch read)
 (println "Hello -- oh, what's your name?")
 (resume (read-line))])

design

The design of cio is primarily modelled after Effekt: though lacking the types, of course.
cio provides several primitives: define-effect, raise-effect, try, resume, and with-effect-handler. The Racket implementation is slightly more fully-featured than the Guile Scheme implementation and also features resume/raise-effect, try/shallow, try/pure, and with-deep-effect-handler.

try steps a computation to a value. If in the process of that computation, an effect is raised by raise-effect, and an appropriate handler (case) is provided for it within the try body, the effect is handled there and the computation (may be) resumed.

There is a distinction between deep and shallow try handlers. cio supports both - deep handlers are supported by the try macro, but shallow handlers may be used as well either through try/shallow or the lower-level single-effect with-effect-handler. Shallow handlers only trigger once, while deep handlers will handle future effects after any resumption. Deep handlers are considerably more useful and intuitive, and combined with mutable state can easily model shallow handlers - thus they are the primary focus. However, shallow effect handlers enjoy a more straightforward mapping to systems of delimited continations and prompts -- indeed, with-effect-handler (defined on analogy to R7Rs's with-exception-handler) is exactly an alias to call-with-prompt in the Guile Scheme implementation.

raise-effect takes in a tag and a number of values, corresponding to an effect, and suspends computation up the call stack to the nearest handler. This provides for the separation of meaning and use, and is the core idea behind effect handlers. The invocation/use of raise-effect by itself does not have one fixed meaning -- instead, meaning is ascribed to it by some contextually-salient handler. This turns out to be quite powerful.

resume resumes a computation, optionally with a value. resume/raise-effect resumes a computation with an effect, to be immediately raised. They are only usable within the handlers of a try block. When an effect is raised from the computation of a try block, and caught by a handler, a continuation to the rest of the computation is bound to a hidden continuation variable, which resume implicitly operates upon. This means that it can be difficult to operate on an outer try block within another nested try block - however, resume is expanded before being packed up in a function, meaning this can be worked around with mutable state.

Resumption is an extremely powerful feature. Suspension alone allows the implementation of exceptions. Suspension and resumption without a value allows the implementation of generators. Suspension and resumption with a value allows the implementation of async/await. Suspension and resumption with another effect allows the implementation of bidirectional control flow. This makes for a very powerful - possibly too powerful - abstraction over all non-local control flow. Whether this can be done in a controlled and usable fashion is an active area of research.

bibliography

A lot of wonderful papers on effect handlers have been published, most of them in the last couple of years. If you are interested in learning more, I recommend reading the following papers / listening to the following talks, in order:

  1. Deliminated Continuations, Demystified (talk only)
  2. Effects as Capabilities (talk)
  3. Handling Bidirectional Control Flow (talk)
  4. Effects, Capabilities, and Boxes (talk)
  5. Continuing WebAssembly with Effect Handlers (talk)
  6. Soundly Handling Linearity (talk)
  7. Lexical Effect Handlers, Directly

Other works can be found at the community-maintained effects bibliography.