SML/NJ Reactive Library: tutorial
A Reactive library is distributed with SML/NJ, but the documentation is limited. It is described by its author, Riccardo Pucella, in the paper Reactive Programming in Standard ML. This page briefly describes the library and gives examples of its use.
A detailed description of the programming model and its motivation are given on the Reactive Programming site of Frédéric Boussinot.
The ReactiveML programming language of Louis Mandel is a modern mix of reactive programming and ML (OCaml)—it is language rather than library based.
Patch
There are bugs in the library up to at least SML/NJ 110.60. This patch can be applied to (a copy of) the
src/smlnj-lib/Reactive
directory.
Introduction
The Reactive library of SML/NJ implements the reactive programming approach of SugarCubes, which was inspired by the synchronous language Esterel. Reactive programs respond to events (input signals) over sequences of discrete instants. The detection and rejection of incoherent or non-deterministic programs is avoided in SugarCubes by restricting judgements on signal absence to the end of an instant, and only allowing weak preemption. This greatly simplifies issues of composition and modularity at the cost of a restricted programming model and the possibility that a response to an event may occur over several reactions.
Programs are built from instruction
values, they are
executed by constructing machine
values. Communication with
and within a program is via pure signals. The type signal
is
a synonym for Atom.atom
. Expressions over signals
are built in the config
type.
A simple program
The library routines are introduced into the top-level environment via these commands:
CM.make "$/reactive-lib.cm"; open Reactive; infix || &;
A simple program might print the string Hello in the first
instant, and the string World in the second. SML commands are
included in a Reactive program by wrapping them as action
instructions:
val printHello = action (fn _=> TextIO.print "Hello\n"); val printWorld = action (fn _=> TextIO.print "World\n");
Here we ignore the argument of type machine
passed to
actions because our commands do not depend on signals in the execution
environment.
The stop
instruction ends a reaction instant. Instructions
may be sequenced with the &
operator—more usually
written as a semi-colon (;
):
val hellomac = machine {body = printHello & stop & printWorld, inputs=[], outputs=[]};
This machine does not have any input or output signals, it
processes a single reaction at each run
command:
run hellomac; (* Reaction 1 displays: Hello *) run hellomac; (* Reaction 2 displays: World *)
The run
command returns false
if further
reactions are possible, and true
if the program has
terminated
Processing both instants a second time is possible after resetting the machine:
reset hellomac;
Using signals
A more interesting program would respond to external input signals.
Signals are created by passing a name to the atom
constructor:
val TOGGLE = Atom.atom "TOGGLE"; val ON = Atom.atom "ON"; val OFF = Atom.atom "OFF";
The await
instruction blocks until the specified signal is
present:
val awaitToggle = await (posConfig TOGGLE);
The posConfig
constructor is necessary because
await
is able to respond to more general expressions over
events.
Signals are emitted with the emit
instruction. A simple
program responds to TOGGLE
signals by alternately emitting
ON
and OFF
:
val alt = machine {body= loop (awaitToggle & (emit ON) & stop & awaitToggle & (emit OFF) & stop), inputs=[TOGGLE], outputs=[ON, OFF]};
The signal values used previously (TOGGLE
, ON
and OFF
) are just names. The ‘live’ signal
identifiers must be extracted from the machine using inputsOf
and outputsOf
, before they may be changed or queried:
val [altTOGGLE] = inputsOf alt; val [altON, altOFF] = outputsOf alt;
These values are associated with the specified machine. The inputs may
be set before running a reaction with setInSignal
. The
outputs may be queried after running a reaction with
getOutSignal
:
run alt; map getOutSignal [altON, altOFF]; setInSignal (altTOGGLE, true); run alt; map getOutSignal [altON, altOFF]; setInSignal (altTOGGLE, true); run alt; map getOutSignal [altON, altOFF];
The outputs have the value true
iff they were emitted in
the previous run.
A final example: ABRO
The ABRO example is described by
Gérard Berry in the Esterel
primer as a means of contrasting the Write Things Once (WTO)
principle of Esterel with a finite state machine specification. It is also
a simple example of the parallel (||
) and preemption
(trapWith
) operators:
val A = atom "A"; val B = atom "B"; val R = atom "R"; val O = atom "O"; fun await_notr s = await (andConfig (posConfig s, negConfig R)); val halt = loop stop; val abroprog = stop & ( loop ( trapWith (posConfig R, ((await_notr A) || (await_notr B)) & emit O & halt, stop ))); val abro = machine {inputs=[A, B, R], outputs=[O], body=abroprog}; map (Atom.toString o inputSignal) (inputsOf abro); map (Atom.toString o outputSignal) (outputsOf abro); val [A, B, R] = inputsOf abro; val [O] = outputsOf abro;
Since the Reactive library does not support strong preemption, the
await
statements must check that R
is absent by
watching a configuration equivalent to: S and (not R)
where
S
is either A
or B
. Not quite
WTO!
The program emits O
when both A
and
B
have been received; in any order or simultaneously. The
R
signal causes the program to forget any previously received
signals. In contrast to Esterel, the stop
instructions
initially and in the trapWith
handler are necessary because
the Reactive library await
instruction is
immediate (it may begin and terminate in the same reaction).
Without the stop
handler the loop becomes
instantaneous.
The program may be exercised in the standard way:
run abro; getOutSignal O; app setInSignal [(A, true), (B, false), (R, false)]; val terminated = run abro; val O_present = getOutSignal O; app setInSignal [(A, false), (B, true), (R, false)]; val terminated = run abro; val O_present = getOutSignal O; app setInSignal [(A, false), (B, false), (R, true)]; val terminated = run abro; val O_present = getOutSignal O; app setInSignal [(A, true), (B, true), (R, false)]; val terminated = run abro; val O_present = getOutSignal O; reset abro;
Final remarks
Comments and corrections are encouraged. Please email them.
The materials on SugarCubes clearly and thoroughly describe the programming model implemented by the Reactive library.