Effect Racket
(require effect-racket) | package: effect-racket-lib |
This package has not been officially released. Backwards compatibility is not guaranteed.
This package provides support for effect handlers, both as a library, and as a language. Here is an implementation of first-class mutable references:
(struct box (default)) (effect box-get (b)) (effect box-set (b v)) (define (store-service [store (hasheq)]) (handler [(box-get b) (define r (hash-ref store b (box-default b))) (continue r)] [(box-set b v) (define store* (hash-set store b v)) (with ((store-service store*)) (continue* (void)))]))
We define a struct named box that will hold a default value, i.e., the value to return if the box has no mapping in the store. Two operations, declared with effect, will be interpreted by an effect handler. The store-service function takes in a store and returns a first-class handler that interprets box effects in that store.
For box-get, we look up the given box in the store. If the box has a mapping in the store, then the mapped value is returned. Otherwise, the default value is returned. To return a value, the continue function is invoked. This function is bound to a delimited continuation from the point where the effect was requested up to and including the handler itself. In the effect handler literature, this is known as a deep handler.
For box-set, we construct a new store that maps the box to its new value. The continuation is then executed in a context where operations are handled by a new handler, with the updated store. This time, we use continue* to invoke the delimited continuation up to but not including the handler itself. In the effect handler literature, this is known as a shallow handler. Using continue* allows us to reinterpret subsequent effects using a different handler.
Here is how this box implementation is used:
> (with ((store-service)) (define b (box 0)) (box-set b (add1 (box-get b))) (box-get b)) 1
1 Language
#lang effect/racket | package: effect-racket-lib |
A language, effect/racket, provides effect handlers and ensures that all built-in operations cooperate with effect handlers too.
This program reinterprets read to take input from a list of strings, rather than standard in.
#lang effect/racket (define (add-from-inputs) (+ (read) (read))) (define (fixed-input-service ins) (handler [(read) (with ((fixed-input-service (cdr ins))) (continue* (car ins)))]))
The function fixed-input-service takes in a list of values and feeds that list to read, one by one. Here is how that might be used:
> (with ((fixed-input-service '(1 2))) (add-from-inputs)) 3
Note that an effect/racket module can only require from other effect/racket modules. There is no interoperability between effect/racket and other languages. Higher-order values imported across the language boundary will be sealed, i.e, unusable.
2 Effects
syntax
(effect id (param ...))
id as a procedure that performs the given effect. This procedure accepts an optional keyword argument #:fail that, given a failure-result/c, calls that thunk (or yields that value) when no handler exists for the effect in the current context.
id as a match pattern that matches effect values created by id.
id? is a predicate that succeeds on effect values created by id.
procedure
(effect-value? v) → boolean?
v : any/c
3 Handlers
syntax
(handler [pat body ...+] ...)
Both continue and continue* are bound in the bodies of each handler arm to a deep and shallow delimited continuation, respectively.
syntax
(contract-handler [pat body ...+] ...)
> (effect increment ())
> (define (limited? _) (< (increment) 2))
> (define/contract (f x) (-> limited? any) x)
> (define (increment-service n) (contract-handler [(increment) (values n (increment-service (add1 n)))]))
> (with ((increment-service 0)) (f 1) (f 1)) 1
> (with ((increment-service 0)) (f 1) (f 1) (f 1)) f: contract violation
expected: limited?
given: 1
in: the 1st argument of
(-> limited? any)
contract from: (function f)
blaming: top-level
(assuming the contract is correct)
at: eval:12:0
procedure
(handler-append v ...) → handler?
v : handler?
> (effect go (x))
> (define twice-service (handler [(go x) (continue (go (* x 2)))]))
> (define decr-service (handler [(go x) (continue (sub1 x))]))
> (define twice-then-decr-service (handler-append decr-service twice-service))
> (with (twice-then-decr-service) (go 10)) 19
syntax
(with (handler ...) body ...+)
4 Contracts
> (define pure/c (->e none/c any/c))
> (define/contract (my-map f xs) (-> pure/c list? list?) (map f xs)) > (my-map (λ (x) (add1 x)) '(1 2 3)) '(2 3 4)
> (my-map (λ (x) (write x) x) '(1 2 3)) my-map: contract violation;
none/c allows no values
given: (write 1)
in: the performed effect
the 1st argument of
(->
(->e none/c ...rivate/contract.rkt:40:3)
(listof any/c)
(listof any/c))
contract from: (function my-map)
blaming: top-level
(assuming the contract is correct)
at: eval:22:0
procedure
(dependent->e eff make-ret) → contract?
eff : contract? make-ret : (-> effect-value? contract?)
> (effect id (v))
> (define/contract (f) (dependent->e id? (match-lambda [(id v) (=/c v)])) (id 42))
> (with ((handler [(id v) (continue v)])) (f)) 42
> (with ((handler [(id v) (continue 0)])) (f)) f: contract violation
expected: (=/c 42)
given: 0
in: the effect response
(->e id? composed)
contract from: (function f)
blaming: top-level
(assuming the contract is correct)
at: eval:26:0
> (effect id-callable? ())
> (define no-id/c (let () (define no-id-handler (contract-handler [(id-callable?) (values #f no-id-handler)])) (with/c no-id-handler)))
> (define/contract (do-it thk) (-> no-id/c any) (thk))
> (define/contract (id x) (->* (any/c) #:pre (id-callable?) any) x) > (do-it (λ () (+ 1 1))) 2
> (do-it (λ () (id 1))) id: contract violation
#:pre condition
in: (->* (any/c) #:pre ... any)
contract from: (function id)
blaming: top-level
(assuming the contract is correct)
at: eval:32:0