Event-lang:   synchronizable event programming
1 Introduction
1.1 Some examples
2 Reference
2.1 Sequential Combinators
pure
return
args
args*
fmap
fmap*
app
app*
bind
bind*
seq
seq0
test
series
series*
reduce
reduce*
loop
loop*
2.2 Concurrent Combinators
async-set
async-set*
async-args
async-args*
async-fmap
async-fmap*
async-app
async-app*
async-bind
async-bind*
2.3 Synchronization Gates
gate?
gate
open-gate
gated
2.4 Racket
2.4.1 Syntactic Forms
event-let
event-let*
event-cond
2.4.2 Pairs and Lists
event-list
event-list*
event-map
2.4.3 Concurrent Syntactic Forms
async-let
2.4.4 Concurrent Pairs and Lists
async-list
async-list*
async-map
async-void
async-void*
6.12

Event-lang: synchronizable event programming

Eric Griffis <[email protected]>

    1 Introduction

      1.1 Some examples

    2 Reference

      2.1 Sequential Combinators

      2.2 Concurrent Combinators

      2.3 Synchronization Gates

      2.4 Racket

        2.4.1 Syntactic Forms

        2.4.2 Pairs and Lists

        2.4.3 Concurrent Syntactic Forms

        2.4.4 Concurrent Pairs and Lists

1 Introduction

Event-lang is a Racket library that simplifies the creation of complex synchronizable events. It provides a primitive expression lifting form,

> (pure 123)

#<evt>

some event combinators,

> (sync (fmap + (pure 1) (pure 2)))

3

> (sync (app (pure +) (pure 1) (pure 2)))

3

> (sync (bind (pure 1) (pure 2) (λ xs (pure (apply + xs)))))

3

and a collection of event-friendly alternatives to base Racket forms and functions.

> (sync
   (event-let
    ([x (pure 1)]
     [y (pure 2)])
    (pure (list x y))))

'(1 2)

Composite events make progress by synchronizing constituent events, either concurrently or in a predictable sequence. Synchronization results can be ordered as specified,

> (let ([t0 (current-inexact-milliseconds)])
    (define (now) (- (current-inexact-milliseconds) t0))
    (sync
     (async-args
      (pure (cons 1 (now)))
      (pure (cons 2 (now)))
      (pure (cons 3 (now))))))

'(1 . 0.50390625)

'(2 . 0.3740234375)

'(3 . 0.5380859375)

or as completed.

> (let ([t0 (current-inexact-milliseconds)])
    (define (now) (- (current-inexact-milliseconds) t0))
    (sync
     (async-set
      (pure (cons 1 (now)))
      (pure (cons 2 (now)))
      (pure (cons 3 (now))))))

'(1 . 0.20703125)

'(2 . 0.248046875)

'(3 . 0.27099609375)

The project has three outstanding objectives:

  1. Provide a sophisticated lifting form to simplify usage of the provided constructs.

  2. Provide a full-blown #lang event/racket/base for producing whole modules of events and event constructors from ordinary Racket code in a principled manner.

  3. Provide support for static analysis of synchronization behaviors. Event programming in Racket is a curious form of meta-programming, and a few simple compile-time checks could reduce cognitive overhead.

1.1 Some examples

Wait until a bunch of stuff is done
> (define ch (make-channel))
> (sync
   (async-void*
    (append (for/list ([i 10])
              (thread (λ () (channel-put ch i))))
            (for/list ([_ 10])
              (thread (λ () (write (channel-get ch))))))))

9876543210

Do the same thing many times
> (define (channel-dup-evt cs v)
    (async-void* (map (curryr channel-put-evt v) cs)))

With some background getters,

> (define cs (build-list 5 (λ _ (make-channel))))
> (define ts
    (for/list ([c cs] [i 5])
      (thread (λ () (writeln (cons i (channel-get c)))))))

it’s ready for synchronization.

> (sync (seq (channel-dup-evt cs 'X) (async-void* ts)))

(2 . X)

(1 . X)

(3 . X)

(4 . X)

(0 . X)

Generate the natural numbers
> (define nat
    (let ([n 0]) (pure (begin0 n (set! n (add1 n))))))

nat acts like a generator.

> (sync nat)

0

> (sync nat)

1

> (sync nat)

2

nat is handy for generating indices and unique keys in bulk through repetition.

> (sync (event-list* (make-list 4 nat)))

'(3 4 5 6)

Fibonacci numbers
> (define (naive-fib n)
    (case n
      [(0) (pure 0)]
      [(1) (pure 1)]
      [else (fmap + (naive-fib (- n 1)) (naive-fib (- n 2)))]))

Of course, the naive implementation is very slow.

> (time (sync (naive-fib 29)))

cpu time: 5826 real time: 5831 gc time: 1004

514229

This one:

> (define fib
    (let ([a 1] [b 0])
      (pure (begin0 b (set!-values (a b) (values (+ a b) a))))))

is much faster.

> (time (last (sync (event-list* (make-list 30 fib)))))

cpu time: 0 real time: 1 gc time: 0

514229

nat and fib can be combined to build an index.

> (define fibs (make-hash))
> (sync
   (async-void*
    (make-list 30 (fmap (curry hash-set! fibs) nat fib))))
> (hash-ref fibs 29)

514229

> (hash-ref fibs 15)

610

Promises
> (define (promise thunk)
    (define result #f)
    (bind (thread (λ ()
                    (define vs (call-with-values thunk list))
                    (set! result (pure (apply values vs)))))
          (λ _ result)))

The results are memoized so multiple syncs don’t replay side effects.

> (define p (promise (λ () (writeln 123) 4)))

123

> (sync p)

4

> (sync p)

4

2 Reference

 (require event) package: event-lang

2.1 Sequential Combinators

syntax

(pure datum)

Lifts datum into a into a synchronizable event. Delays evaluation of datum until a thread synchronizes on it. The synchronization result is the evaluation result.

> (define evt (pure (writeln (+ 1 2))))
> (sync evt)

3

> (sync evt)

3

procedure

(return v)  evt?

  v : any/c
 = (pure v)
Evaluates v and then lifts the result into an event. Returns a synchronizable event that does nothing and uses v as its synchronization result.

> (define evt (return (writeln (+ 1 2))))

3

> (sync evt)
> (sync evt)

procedure

(args E ...)  evt?

  E : evt?

procedure

(args* Es)  evt?

  Es : (listof evt?)
Returns a synchronizable event that evaluates Es in order and then applies values to the synchronization results.

> (sync (args (pure 1) (pure 2) (pure 3)))

1

2

3

procedure

(fmap f E ...)  evt?

  f : (-> any/c ... any)
  E : evt?

procedure

(fmap* f Es)  evt?

  f : (-> any/c ... any)
  Es : (listof evt?)
Returns a synchronizable event that synchronizes Es in order and then applies f to the synchronization results.

> (sync (fmap + (pure 1) (pure 2) (pure 3)))

6

procedure

(app F E ...)  evt?

  F : evt?
  E : evt?

procedure

(app* F Es)  evt?

  F : evt?
  Es : (listof evt?)
Returns a synchronizable event that synchronizes F and Es in order and then applies the synchronization result of the former to the synchronization results of the latter.

> (sync (app (pure +) (pure 1) (pure 2) (pure 3)))

6

procedure

(bind E ... f)  evt?

  E : evt?
  f : (-> any/c ... evt?)

procedure

(bind* Es f)  evt?

  Es : (listof evt?)
  f : (-> any/c ... evt?)
Returns a synchronizable event that synchronizes Es in order and then becomes the event returned from f applied to the synchronization results.

> (sync
   (bind
    (pure 1)
    (pure 2)
    (pure 3)
    (compose return +)))

6

procedure

(seq E ...+)  evt?

  E : evt?
Returns a synchronizable event that synchronizes Es in order and then uses the final synchronization result as its own.

> (sync (seq (pure 1) (pure 2) (pure 3)))

3

procedure

(seq0 E ...+)  evt?

  E : evt?
Returns a synchronizable event that synchronizes Es in order and then uses the first synchronization result as its own.

> (sync (seq0 (pure 1) (pure 2) (pure 3)))

1

procedure

(test E1 E2 E3)  evt?

  E1 : evt?
  E2 : evt?
  E3 : evt?
Returns a synchronizable event that becomes either E2 or E3. If the synchronization result of E1 is not #f, it becomes E2. Otherwise, it becomes E3.

> (sync (test (pure #t) (pure 1) (pure 2)))

1

> (sync (test (pure #f) (pure 1) (pure 2)))

2

procedure

(series E f ...)  evt?

  E : evt?
  f : (-> any/c evt)

procedure

(series* E fs)  evt?

  E : evt?
  fs : (listof (-> any/c evt?))
Returns a synchronizable event that applies fs in series, starting with the synchronization result of E and continuing with the synchronization result of the event generated by the previous f. Uses the final synchronization result as its own.

> (sync
   (series
    (pure 1)
    (λ (x) (return (+ x 2)))
    (λ (x) (return (* x 3)))))

9

procedure

(reduce f check v ...)  evt?

  f : (-> any/c ... evt?)
  check : (-> any/c ... boolean?)
  v : any/c

procedure

(reduce* f check vs)  evt?

  f : (-> any/c ... evt?)
  check : (-> any/c ... boolean?)
  vs : (listof any/c)
Returns a synchronizable event that applies f to a set of values recursively, starting with v ... or vs and continuing with the synchronization result of the event generated by applying f to the previous results. Applies check to an argument list created by appending v ... or vs onto the results of f. Becomes ready for synchronization when check returns #t. Uses the final synchronization result as its own.

> (sync
   (reduce
    (λ (x) (pure (add1 x)))
    (λ (x y) (>= y 10))
    0))

10

procedure

(loop f v ...)  evt?

  f : (-> any/c ... evt?)
  v : any/c

procedure

(loop* f vs)  evt?

  f : (-> any/c ... evt?)
  vs : (listof any/c)
Returns a synchronizable event that applies f to a value recursively, starting with v ... or vs and continuing with the synchronization result of the event generated by applying f to the previous results. Never becomes ready for synchronization.

> (with-handlers ([number? values])
    (sync
     (loop (λ (x) (if (< x 10) (pure (+ x 1)) (raise x)))
           0)))

10

2.2 Concurrent Combinators

procedure

(async-set E ...)  evt?

  E : evt?

procedure

(async-set* Es)  evt?

  Es : (listof evt?)
Returns a synchronizable event that synchronizes E ... or Es concurrently and then applies values to a list of the synchronization results in order of completion.

> (define evt
    (handle-evt (async-set (pure 1) (pure 2) (pure 3)) list))
> (sync evt)

'(1 2 3)

> (sync evt)

'(3 1 2)

> (sync evt)

'(1 3 2)

procedure

(async-args E ...)  evt?

  E : evt?

procedure

(async-args* Es)  evt?

  Es : (listof evt?)
Returns a synchronizable event that evaluates E ... or Es concurrently and then applies values to a list of the synchronization results in the order defined.

> (define evt
    (handle-evt
     (async-args (pure 1) (pure 2) (pure 3))
     list))
> (sync evt)

'(1 2 3)

procedure

(async-fmap f E ...)  evt?

  f : (-> any/c ... any)
  E : evt?

procedure

(async-fmap* f Es)  evt?

  f : (-> any/c ... any)
  Es : (listof evt?)
Returns a synchronizable event that synchronizes E ... or Vs concurrently and then applies f to a list of the synchronization results.

> (sync (async-fmap + (pure 1) (pure 2) (pure 3)))

6

procedure

(async-app F E ...)  evt?

  F : evt?
  E : evt?

procedure

(async-app* F Es)  evt?

  F : evt?
  Es : (listof evt?)
Returns a synchronizable event that synchronizes F and E ... or Es concurrently and then applies the synchronization result of the former to a list of the synchronization results of the latter.

> (sync (async-app (pure +) (pure 1) (pure 2) (pure 3)))

6

procedure

(async-bind E ... f)  evt?

  E : evt?
  f : (-> any/c ... evt?)

procedure

(async-bind* Es f)  evt?

  Es : (listof evt?)
  f : (-> any/c ... evt?)
Returns a synchronizable event that synchronizes E ... or Es concurrently and then becomes the event returned from f applied to a list of the synchronization results.

> (sync
   (async-bind
    (seq (pure (print 1)) (pure 1))
    (seq (pure (print 2)) (pure 2))
    (seq (pure (print 3)) (pure 3))
    (compose return list)))

312

'(1 2 3)

2.3 Synchronization Gates

 (require event/gate) package: event-lang

A gate is a simple primitive for synchronizing many threads at once. A gate is either opened or closed and is closed initially. Threads synchronizing on a closed gate will block until the gate is opened. Once a gate is opened, it cannot be closed.

procedure

(gate? v)  boolean?

  v : any/c
Returns #t if v is a gate, #f otherwise.

procedure

(gate)  gate?

Creates and returns a new closed gate.

procedure

(open-gate g)  evt?

  g : gate?
Returns a synchronizable event that simultaneously unblocks all threads attempting to synchronize on the gate. Becomes ready for synchronization when the gate is opened.

procedure

(gated g E)  evt?

  g : gate?
  E : evt?
Returns a synchronizable event that synchronizes E and becomes ready for synchronization when g is opened. The synchronization result is the synchronization result of E.

2.4 Racket

 (require event/racket) package: event-lang

2.4.1 Syntactic Forms

syntax

(event-let ([id val-evt] ...) body-evt ...+)

Creates a synchronizable event that synchronizes the val-evts from left to right and binds the ids to the results, then synchronizes the body-evts. Uses the synchronization result of its final body-evt as its own.

> (sync
   (event-let ([x (pure 1)]
               [y (pure 2)])
     (pure (+ x y))))

3

syntax

(event-let* ([id val-evt] ...) body-evt ...+)

Like event-let, but synchronizes the val-evts one by one, binding each id as soon as the value is available. The ids are bound in the remaining val-evts as well as the bodys, and the ids need not be distinct; later bindings shadow earlier bindings.

Creates a synchronizable event that Synchronizes the val-evts from left to right and binds the ids to the results, then synchronizes the body-evts. Uses the synchronization result of its final body-evt as its own.

> (sync
   (event-let* ([x (pure 1)]
                [y (pure (+ x 2))])
     (pure (+ x y))))

4

syntax

(event-cond event-cond-clause ...)

 
event-cond-clause = [test-evt then-body-evt ...+]
  | [else then-body-evt ...+]
  | [test-evt => proc-evt]
  | [test-evt]
Creates a synchronizable event. If no event-cond-clauses are present, the synchronization result is #<void>.

An event-cond-clause that starts with else must be the last event-cond-clause.

If only a [else then-body-evt ...+] is present, then the then-body-evts are synchronized. The synchronization result from all but the last then-body-evt are ignored. The synchronization result of the last then-body-evt is the synchronization result for the whole event-cond form.

Otherwise, the first test-evt is synchronized. If it produces #f, then the synchronization result is the same as an event-cond form with the remaining event-cond-clauses.

[test-evt then-body-evt ...+]

The then-body-evts are synchronized in order, and the synchronization result from all but the last then-body-evt are ignored. The synchronization result of the last then-body-evt provides the result for the whole event-cond form.

[test-evt => proc-evt]

The proc-evt is synchronized, and it must produce a procedure that accepts one argument, otherwise the exn:fail:contract exception is raised. The procedure is applied to the synchronization result of test-evt. The synchronization result for the whole event-cond form is the values returned by the procedure call.

[test-evt]

The synchronization result of test-evt is provided as the synchronization result of the event-cond form.

Examples:
> (sync (event-cond))
> (sync (event-cond [else (pure 5)]))

5

> (sync
   (event-cond
    [(pure (positive? -5)) (pure (error "doesn't get here"))]
    [(pure (zero? -5)) (pure (error "doesn't get here, either"))]
    [(pure (positive? 5)) (pure 'here)]))

'here

> (sync
   (event-cond
    [(pure (member 2 '(1 2 3))) => (pure (lambda (l) (map - l)))]))

'(-2 -3)

> (sync (event-cond [(pure (member 2 '(1 2 3)))]))

'(2 3)

2.4.2 Pairs and Lists

procedure

(event-list E ...)  evt?

  E : evt?

procedure

(event-list* E ... Es)  evt?

  E : evt?
  Es : (listof evt?)
Returns a synchronizable event that synchronizes all Es in order and then uses a list of the results as its synchronization result.

> (sync (event-list (pure 1) (pure 2) (pure 3)))

'(1 2 3)

procedure

(event-map f Es ...+)  evt?

  f : procedure?
  Es : (listof evt?)
Returns a synchronizable event that synchronizes the elements of the Es lists and applies f to the synchronization results of the elements, from the first elements to the last. The f argument must accept the same number of arguments as the number of supplied Ess, and all Ess must have the same number of elements. The synchronization result is a list containing each result of f in order.

> (sync
   (event-map
    +
    (list (pure 1) (pure 2) (pure 3))
    (list (pure 4) (pure 5) (pure 6))))

'(5 7 9)

2.4.3 Concurrent Syntactic Forms

syntax

(async-let ([x Ex] ...) E ...+)

Produces a synchronizable event that synchronizes Exs concurrently, binds the synchronization results to xs internally, and synchronizes the Es. The synchronization results from all but the last E are ignored. The synchronization result of the last E is the synchronization result for the whole async-let form.

> (sync
   (async-let
       ([x (seq (pure (print 1)) (pure 1))]
        [y (seq (pure (print 2)) (pure 2))]
        [z (seq (pure (print 3)) (pure 3))])
     (pure (values x y z))))

123

1

2

3

2.4.4 Concurrent Pairs and Lists

procedure

(async-list E ...)  evt?

  E : evt?

procedure

(async-list* E ... Es)  evt?

  E : evt?
  Es : (listof evt?)
Returns a synchronizable event that synchronizes all Es simultaneously and becomes ready for synchronization when all the Es are ready. The synchronization result is a list of the results, in order.

> (sync (async-list (pure 1) (pure 2) (pure 3)))

'(1 2 3)

procedure

(async-map f Es ...+)  evt?

  f : procedure?
  Es : (listof evt?)
Returns a synchronizable event that synchronizes Es lists simultaneously and then applies f to the synchronization results of the elements, from the first elements to the last. The f argument must accept the same number of arguments as the number of supplied Ess, and all Ess must have the same number of elements. The synchronization result is a list containing each result of f in order.

> (sync
   (async-map
    +
    (list (pure 1) (pure 2) (pure 3))
    (list (pure 4) (pure 5) (pure 6))))

'(5 7 9)

procedure

(async-void E ...)  evt?

  E : evt?

procedure

(async-void* E ... Es)  evt?

  E : evt?
  Es : (listof evt?)
Returns a synchronizable event that synchronizes all Es simultaneously and becomes ready for synchronization when all the Es are ready. The synchronization result is a single void.

> (sync (async-void (pure 1) (pure 2) (pure 3)))