Reactor: a synchronous reactive language
Warning: This API is unstable, and may change without warning.
(require reactor) | package: reactor |
Reactor is a synchronous reactive language in the style of ReactiveML. A program is represented by a reactor?.
The run of one program is broken up into reactions (also called instants), each of
which can be though of as being instantaneous—
Every expression in the program either takes zero time (e.g. completes in the current reaction) or pauses, which "consumes" time, stopping the computation there until the next reaction.
The code within a reactor? can be a mix of both reactive code (like par&) and non-reactive code (e.g. normal racket expressions). This non-reactive will never consume time and should always terminate.
In general racket level side effects (mutation, non-termination, etc) may break the guarantee of determininistic concurrencuy.
1 Running Programs
procedure
proc : procedure? args : any/c = ...
procedure
r : (and/c reactor? reactor-safe?) start-signals : (or/c pure-signal? (list/c value-signal? (listof any/c)))
2 Creating Reactive Functions
Reactive functions may contain arbitrary racket code. In addition, it may use the use the following forms. By convention forms and functions ending in a & may only be used within the dynamic extent of a reaction.
syntax
Only valid within the dynamic extent of a reaction.
syntax
(par& e ...)
At the end of a reaction if all but one branch has completed that branch becomes in tail position with respect to the par& form.
Only valid within the dynamic extent of a reaction.
> (define (par1) (displayln (par& 1 2))) > (react! (prime par1)) #<void>
> (define (par2) (displayln (par& 1 (begin pause& 2)))) > (define r (prime par2)) > (react! r) > (react! r) 2
syntax
(loop& body ...)
Only valid within the dynamic extent of a reaction.
> (define (loop) (let ([i 0]) (loop& (displayln i) (set! i (+ 1 i)) pause&))) > (define r (prime loop)) > (react! r) 0
> (react! r) 1
> (react! r) 2
> (react! r) 3
syntax
Only valid within the dynamic extent of a reaction.
> (define (halt) (displayln 1) halt& (displayln 2)) > (define r (prime halt)) > (react! r) 1
> (react! r) > (react! r)
> (define (par-halt) (par& (begin (displayln 1) pause& (displayln 2)) (begin (displayln 3) halt& (displayln 4)))) > (define r2 (prime par-halt)) > (react! r2) ; note: these may display in either order, since printing is a side effect
3
1
> (react! r2) 2
> (react! r2)
2.1 Signals
Signals are the core communication mechanism both within a Reactor, and between a reactor and its environment. It is never safe to share a signal between two reactors.
Signals may be either present or absent
within a given instant—
syntax
(define-signal S)
(define-signal S default ...+) (define-signal S default ...+ #:gather gather) (define-signal S default ...+ #:gather gather #:contract contract)
When contract is supplied the signal is protected by that contract. See also signal/c.
The defaut values, gather function, and contract may be supplied in any order.
procedure
S : pure-signal? (emit& S v ...) → void? S : value-signal? v : any/c
Only valid within the dynamic extent of a reaction.
syntax
(present& S then else)
Only valid within the dynamic extent of a reaction.
syntax
(await& maybe-immediate maybe-count signal-expr)
(await& S [pattern body ...] ...+)
signal-expr = signal-or-list | (or signal-or-list ...+) maybe-immediate =
| #:immediate maybe-count =
| #:immediate n
S : signal?
signal-or-list : (or/c signal? (listof signal?))
syntax
(await*& S [(pattern ...) body ...] ...+)
S : signal?
If #:count is provided await& awaits that many emissions of S in as many instants.
If signal-expr is an or clause or a list of signals the await is triggered if any of the signals is present. However if #:count is provided, multiple of these signals being emit&ed counts as only one emission.
If pattern clauses are provided, S must be a value carrying signal. In this case the value is matched against the given patterns in the reaction after S is emitted. It evaluates to the body of the first match. If none match the form continues to await the signal. The await& form matches only signals that carry a single value. The await*& form can match many valued signals.
Only valid within the dynamic extent of a reaction.
procedure
S : value-signal?
procedure
S : value-signal?
> (define-signal input)
> (define/contract (counter input chan) (reactive-> pure-signal? value-signal? none/c) (emit& chan 0) (loop& (await& #:immediate input) (emit& chan (add1 (last chan))) pause&))
> (define/contract (printloop chan) (reactive-> value-signal? none/c) (loop& (await& chan [times (printf "got total of ~a inputs\n" times)])))
> (define/contract (main input) (reactive-> pure-signal? none/c) (define-signal crosstalk 0 #:gather +) (par& (counter input crosstalk) (printloop crosstalk))) > (define r (prime main input)) > (react! r) > (react! r) got total of 0 inputs
> (react! r input) > (react! r) got total of 1 inputs
> (react! r) > (react! r input) > (react! r) got total of 2 inputs
2.2 Control
syntax
(suspend& e ... #:unless signal-expr)
signal-expr = signal-or-list | (or signal-or-list ...+)
signal-or-list : (or/c signal? (listof signal?))
If signal-expr is either an or clause or a list of signals then the suspend executes when any of the given signals are present.
Only valid within the dynamic extent of a reaction.
> (define (hi unlock) (suspend& (loop& (displayln 'hello) pause&) #:unless unlock)) > (define-signal print) > (define r (prime hi print)) > (react! r print) hello
> (react! r) > (react! r) > (react! r print) hello
> (reactor-suspended? r) #t
syntax
(abort& e ... #:after S [pattern body ...] ...)
If patterns are provided, they are matched against the value carried by S. The form evaluates to the body of the first clauses that matches. If non match the execution of e continues.
If e completes before S is emitted and the body is aborted, the form evaluates to the result of e.
Only valid within the dynamic extent of a reaction.
> (define (annoying silence) (abort& (loop& (displayln "I know a song that gets on everybody's nerves") pause&) #:after silence)) > (define-signal off) > (define r (prime annoying off)) > (react! r) I know a song that gets on everybody's nerves
> (react! r) I know a song that gets on everybody's nerves
> (react! r off) I know a song that gets on everybody's nerves
> (react! r) > (reactor-done? r) #t
syntax
(with-handlers& body ... #:after-error [a b] ...)
If multiple errors are raised in the same instant they’re handlers are run in parallel, and the result of each thread is collected into a list (as with par&). The order of the list is not specified.
3 Data
procedure
(pure-signal? S) → boolean?
S : any
procedure
(value-signal? S) → boolean?
S : any
syntax
(signal/c c ...)
If form appears syntactically within #:contract option of the define-signal form the contract is checked when: values are emit&ted, values are read from the signal (e.g. via last), and when values are combined via the gather function. If the gather function violates its contract the positive party will be blamed.
If the contract is attached via another form the contract barrier does not cover the gather function. Instead the contract behaves like box or channel contracts, and is only checked when the value is read from or written to.
procedure
(signal-name s) → (and/c symbol? (not/c symbol-interned?))
s : signal?
procedure
(reactor-suspended? r) → boolean?
r : reactor?
procedure
(reactor-done? r) → boolean?
r : reactor?
syntax
(reactive-> dom ... range)
dom : contract?
range : contract?
procedure
(reactor-safe? r) → boolean?
r : reactor?
This check is not thread safe.
4 Continuation Marks
Reactor provides the ability to get the current continuation marks from a paused reactor?. However continuation marks in reactor have fundamental difference from those in racket: They are a tree rather than a list. This is because par& essentually forks the current continuation into several branches. Therefor Reactor mimics the racket continuation marks API, but extends it with trees.
procedure
(reactor-continuation-marks r) → continuation-mark-set-tree?
r : (and/c reactor? reactor-safe?)
procedure
(continuation-mark-set-tree->tree cmst key) → tree?
cmst : continuation-mark-set-tree? key : any/c
procedure
it : any/c
struct
(struct branch (values children) #:transparent) values : list? children : tree?
struct
(struct leaf (values) #:transparent) values : list?
A reactor without active par&s will always be represented by a leaf. A reactor with an par& with have a branch. The branch-values will contain the continuation mark values from above the par&. branch-children will contain a tree for each active branch of the par.
Note that this mean there will always be more than one child of a branch: If a par& has a single branch at the end of a reaction that branch will become in tail position w.r.t the enclosing context of the par&, removing the par& itself.
5 Caveats and unstable API’s
Warning: The API’s and behaviors presented here are especially unstable, and may change without warning.
5.1 Signals and Synchronization
Signals act as synchronizable event, which becomes ready for synchronization at the end of a reaction in which the signal was emit&ted. The synchronization result is the signal itself.
Signals are currently not thread safe: if a signal is used (via last? or last) in a thread different from a reaction where it is being used, extra synchronization must be used to ensure the signal is not look at during a reaction.
5.2 Caveat concerning exception handling and control jumps
Catching an exception using with-handlers or call-with-exception-handler, capturing and and applying a continuation inside of a reaction, or aborting to a prompt inside of a reaction is unsafe if any of these cross a continuation containing any reactive form and do not jump completely outside of the reaction. For example, if an exception passed through a par&, suspend&, or abort& the reactors control structure may become corrupted, and the reaction behavior and the state of its signals is undefined. Control may safely leave the reactor in this way, but the reactor is marked as unsafe.
However, Catching exceptions with with-handlers& is safe.