Effect Racket
1 Language
2 Effects
effect
effect-value?
3 Handlers
handler
contract-handler
handler-append
handler?
continue
continue*
with
splicing-with
4 Contracts
->e
dependent->e
with/  c
8.12

Effect Racket🔗ℹ

Cameron Moy

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

Declares id as an effect with the given parameters. A number of definitions are created by this declaration:
  • 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
Returns if v is an effect value. An effect value is a first-class value that represents a request to perform an effect. These are the values that handlers match on.

3 Handlers🔗ℹ

syntax

(handler [pat body ...+] ...)

Yields an ordinary effect handler that interprets the given effect-value patterns. If no pattern matches the requested effect, then the effect is propagated to the next handler. This form can only handle effects performed in normal code, not code executed while checking a contract.

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 ...+] ...)

Similar to handler, except it only interprets effects performed while checking contracts. The body expressions don’t have direct access to the continuation, rather each body should return several values. Initial values are provided to the continuation, while the final returned value is a handler to be reinstalled.

> (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?
Appends the given handlers.

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

procedure

(handler? v)  boolean?

  v : any/c
Returns if v is a handler (either an ordinary handler or a contract handler).

procedure

(continue v ...)  any

  v : any/c
Bound to the deep delimited continuation in the arms of with.

procedure

(continue* v ...)  any

  v : any/c
Bound to the shallow delimited continuation in the arms of with.

syntax

(with (handler ...) body ...+)

Executes body with the given handlers installed, nested in the given order.

syntax

(splicing-with (handler ...) body ...+)

Variant of with that splices forms into the enclosing definition context (similar to begin).

4 Contracts🔗ℹ

procedure

(->e eff ret)  contract?

  eff : contract?
  ret : contract?
Returns a contract that protects functions by ensuring all effects performed when that function is applied satisfy eff and all values provided to the continuation by a handler satisfy ret.

> (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?)
A dependent variant of ->e where the continuation contract relies on the effect value.

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

procedure

(with/c handler ...)  contract?

  handler : handler?
Returns a contract that protects functions by installing the given contract handler whenever that function is applied.

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