Proof-of-Concept CML-style composable first-class events on top of core.async.
Concurrent ML is a concurrent programming language developed by John Reppy in the late 1980's and early 1990's. It is based on ideas, namely synchronous message passing and non-deterministic choice, that you can find in CSP and Pi-calculus. CML then extends the idea of non-deterministic choice over synchronous channel operations to first-class composable events with negative acknowledgments (nacks).
With plain core.async, one can express a non-deterministic choice over a linear sequence of synchronous get and put operations:
(alt!
<channel> ([<result>] <action>) ;; get operation
[<channel> <value>] ([<sent>] <action>) ;; put operation
...)
With CML, one has the following combinators (and more) for expressing first-class events:
(gete <channel>) ;; An event to take a value on a channel
<channel> ;; and alternative syntax for gete
(pute <channel> <value>) ;; An event to give a value on a channel
[<channel> <value>] ;; and alternative syntax for pute
(choose <event> ...) ;; Non-deterministic choice over events
(wrap <event> <action-fn>) ;; An event with a post synchronization action
(guard <event-thunk>) ;; An event with a pre synchronization action
(with-nack <nack->event-fn>) ;; An event with a pre sync action given a nack
Compared to plain core.async, non-deterministic choices can be easily nested and actions can be attached both before and after synchronization. Furthermore, there is a combinator that provides negative acknowledgment in case an event wasn't ultimately chosen.
The plain core.async alt!
grammar can be expressed using just a subset of the
combinators
(sync!
(choose
(wrap (gete <channel>) (fn [<result>] <action>))
(wrap (pute <channel> <value>) (fn [<sent>] <action>))
...))
where sync!
is an operation that synchronizes on a given event and returns its
result. For convenience, sync!
has an implicit choose
, so the choose
can
be dropped, and we can also use the alternative syntax for gete
and pute
:
(sync!
(wrap <channel> (fn [<result>] <action>))
(wrap [<channel> <value>] (fn [<sent>] <action>))
...)
Written this way, the sync!
expression is just slightly more verbose than the
alt!
expression, and we could certainly add further sugar to make the sync!
version more concise, but the key here is that the combinators choose
, wrap
,
pute
and gete
are just ordinary functions that return values that can be
further manipulated with other combinators, stored in data structures and even
passed through channels.
BTW, note that gete
and pute
don't end with a bang !
. That is because all
the effects are actually performed by sync!
and the values returned by the
event combinators are immutable representations of selective synchronous
operations.
See the examples for further documentation.
The book Concurrent Programming in ML is the most comprehensive introduction to Concurrent ML style programming.
What is interesting is that CML style events do not require significantly more
complicated machinery than what core.async already provides. This
proof-of-concept library hopefully makes that clear. For production use, you'd
want to implement the CML mechanisms directly. Also note the one exception
raised in the implemention here. The alts!
operation
of core.async does not make it possible to distinguish between multiple
different operations on a single channel, which breaks composability.
Copyright © 2015 Vesa Karvonen
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.