Synchron is a synchronous reactive concurrent programming DSL for Haskell.
It is inspired by Céu and Concur and is meant to be an exploration of the synchronous paradigm in Haskell; it is by no means an efficient implementation and so far lacks a formal specification or operational semantics.
In contrast to e.g. FRP or the observer pattern, synchronous programming reifies the program's state machine implicitly in the program flow, as opposed to advancing execution through short lived, disconnected callbacks (which "eliminate any vestige of structured programming, such as support for long-lasting loops and automatic variables"1). Additionally, the execution model, contrary to actor based or multithreaded models, is deterministic and does not require synchronisation primitives2.
Especially Concur has shown that the synchronous programming paradigm need not stay confined to the embedded or hard real time domain, as it seemingly has so far -- indeed, it lends itself extraordinarily well to programming UIs consisting of complex workflows and user interactions. So much so, that it's arguably one of the most interesting advancements in UI programming since Elm/React.
Synchron explores the synchronous paradigm in a more general setting.
In synchronous programming, reactions take no time with respect to the external environment, i.e. an event can both be caused and perceived at the same time.
Synchron allows for running multiple trails simultaneously at logical time steps in reaction to external or internal events.
example :: Syn Int
example = local $ \e -> do ➊
(_, a) <- andd (emit e 5, await e) ➋
(_, b) <- andd (emit e 6, await e) ➌
pure (a + b)
local
(➊) creates a new internal event (the difference between external and internal events will become clearer later). The andd
(➋) combinator forks the execution and starts both trails (emit e 5
and await e
) simultaneously and only continues with the next line (➌) when all trails have finished.
emit
fires the specified event with the given value, while await
blocks until a given event fires.
andd
is overloaded for tuples of arity up to 6, yielding tuples of the same arity in the Syn
monad. This allows for trails with differing types, in contrast to orr
(alternatively, andd'
takes a monomorphic list if that is desired).
TODO State machine in data vs code; "reification of time"; "reinversion of control"
TODO Interesting and unexplored applications of synchronous programming
TODO Contrast with FRP
TODO External events
TODO Connectors
This section explains some low level technicalities of Synchron and can be skipped safely.
From The Céu Manual (v0.30, p. 3):
When multiple trails are active at a logical time (i.e. awaking from the same event), Céu schedules them in the order they appear in the program text. This policy is arbitrary, but provides a priority scheme for trails, and also ensures deterministic and reproducible execution for programs.
In practice, this means that the programs
local $ \e -> andd (await e, emit e 5)
and
local $ \e -> andd (emit e 5, await e)
are not equivalent. Due to Céu's operational semantics, the former will terminate, as expected, while the latter will block forever on await e
. In the context of Haskell this seems unintuitive and non-compositional: a trail definition, which might come from a completely different module or package, must be aware of (or alternatively, specify) the execution order in parallel statements to ensure correctness.
To combat this problem, Synchron takes a different route. Program execution is divided into three distinct phases:
emit
, await
or a parallel combination thereof (in andd
or orr
) is encountered. In other words, perform all straightforward statements, like local
or async
.emit
statements and combine with all currently firing external events. Don't advance the program further.await
statements affected by the firing events gathered in the previous phase. Advance all emit
statements. Advance orr
if at least one of its trails has finished. Advance andd
if and only if all of its trails have finished.The advance - gather - unblock phases are repeated in that order until the unblock phase can not advance the program further (at which point the program has either finished or is blocked). Although not formally proven, this execution model seems to facilitate trail order invariance in andd
while remaining deterministic and reproducible.
[1] F. Sant’Anna et al. Structured Synchronous Reactive Programming with Céu