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.