Recently David Nolen has written about how a combination of event stream processing and communicating sequential processes can be used to simplify user interface programming.
He proposes a novel three part architecture consisting of:
- Event stream processing
- Event stream coordination
- Interface representation
I'm quite taken with stream processing, so much so that I'm writing a ClojureScript library that enables it. Interface representation is a brilliant idea and I wish I'd thought of it before. However stream coordination was new to me and it is the main focus of this response.
Stream Coordination Examples
Nolen gives no strict definition for stream coordination, instead he illustrates with examples. To me the examples look more complex, and less functional, than raw stream processing. So I'm left feeling that stream coordination should be avoided.
The coordination functions selector
and highlighter
take and return
core.async channels. This is great as it means these processes
don't care where the events come from or end up. Composing them extends the
functionality of the user interface. But there are some drawbacks to this
approach:
- Neither function is pure. They read values out of the channel. This both mutates the channel (removing the value) and means we can not determine the return values purely from the function arguments.
- Recognition, and processing of events are handled in the same function. A simpler design would split these responsibilities.
- Explicit flow control (
loop
/recur
) and event emission (>!
) are required. Higher-order functions could eliminate both of these chores. - The functions emit only unknown events. This means they must assume all responsibility for those events which they process. This is less flexible than allowing for multiple consumers of each channel.
Raw stream processing
I've implemented the highlight / selection example using raw stream processing. Click in the box to give it focus then use up, down, j, k and enter to change highlight and make selections.
Interactive Example
You can see the full code on github but I've included the meat of it here. It's written using promise-streams which aim to provide event streams in an idiomatic Clojure way. They're implemented as promises wrapped around cons cells, and provide asynchronous versions of Clojure's sequence functions.
; Pure stream processing
(defn identify-actions [keydowns]
(->> keydowns
(mapd* #(aget % "which"))
(mapd* keycode->key)
(filter* (comp promise identity))
(mapd* key->action)))
(defn track-highlight [wrap-at actions]
(->> actions
(filter* (comp promise highlight-actions))
(mapd* highlight-action->offset)
(reductions* (fmap +) (promise 0))
(mapd* #(mod % wrap-at))))
(defn track-ui-states [actions highlight-indexes]
(->> (filter* (comp promise select-actions) actions)
(concat* highlight-indexes)
(reductions* (fmap remember-selection) (promise first-state))))
(defn selection [ui keydowns]
(let [actions (identify-actions keydowns)
highlight-indexes (track-highlight (count ui) actions)
ui-states (track-ui-states actions highlight-indexes)]
(mapd* (partial render-ui ui) ui-states)))
; Side effects
(defn load-example [ui first-state output]
(->> (sources/callback->promise-stream on-keydown output)
(selection ui)
(mapd* (partial jq/text output)))
(jq/text output (render-ui ui first-state)))
I've created a graph of the data flow through the system. It labels the kinds of events at each step and may help you get a feel for how everything ties together.
This stream processing code addresses my concerns with the stream coordination code.
load-example
grabs events from the document, feeds them through the purely functional code, and finally dumps the rendered ui into the dom. This is what I've come to expect from Clojure code; a thin procedural shell around a delicious functional core.identify-events
recognises events.track-highlight
,track-ui-states
andselection
give the events meaning, manage state and handle rendering.- The functions are just passing the data through a sequence of super simple processing steps. The functions passed into the higher-order functions need not care that they're dealing with streams of events.
- Streams can be re-used without their needing to explicitly allow it.
selection
passes theactions
stream to bothtrack-highlight
andtrack-ui-states
.
My code only takes the first two steps from Nolen's post. It's possible that there are complications introduced from the mouse interactions that haven't occurred to me. But I've previously written the other half of an autocompleter and I think I see how a full stream processing solution would come together.
I'm looking forward to seeing the concluding post in his CSP autocompleter series. I hope that he clarifies exactly what he has in mind by stream coordination. If anyone disagrees with my observations, or has a better understanding of what's going on than I do, please email or tweet at me.