Applying Clojure core.async to JavaFX

01.05.2014 Permalink

If you ever created a non-trivial enterprise rich client application with many UI forms you will have noticed that it is quite a challenge. Given the conditions that apply this is no surprise: As a result rich client code often exhibits overwhelming complexity and quickly becomes unmaintainable. Considering the importance a user interface has for product acceptance and the amount of code necessary to produce a sufficient user experience, this is an unsatisfying situation.

In the past, I worked on several rich clients in OO languages like C++ and Java. The best ideas I was able to find in OO land beyond the very basic - but still somehow applicable - separation of responsibilities into Model, View and Controller (MVC) were Model-View-Presenter and Presentation Model. Both patterns essentially address the problem of how to make presentation logic unit-testable without using GUI robots and brittle test scripts.

An improvement, but still not satisfying

In a customer project two years ago, I used separate classes for data - the Model - and actions - the Controller. I built a textual UI form DSL and a corresponding code generator that produced View classes, and added a full-blown data binding between Model and View. In addition I invented a UI interface to give the Controller a mockable API for UI operations, e.g. creating a new UI form or showing a message box.

Here's an overview of this design:

This already allowed the team to produce unit-testable client code without having to deal with many of the tricky details of GUI development, but the client still heavily used callbacks in disguise of "action methods", and these methods did their work through in-place mutation in Model and Controller instances. Even worse, in order to execute long-running actions, developers still had to create two additional levels of callbacks, one for the long-running operation and the other to merge the results back into the view. This was the forecourt to callback hell, and it is still the default way in JavaFX and Swing to keep the application responsive to user input. So the code expressing the presentation logic was still messy, nonetheless it was the best I was able to come up with by that time.

And now for something completely different

Through learning Clojure I came across ideas like Functional Reactive Programming (FRP) and Communicating Sequential Processes (CSP). These approaches allow us to treat some of these problems in a different way.

In the last few months, I tried to apply FRP in my experimental library visuals, which is based on a home-grown implementation of Signals (a.k.a Behaviours), Eventsources (a.k.a Eventstreams) and corresponding operations, which I re-implemented in fall 2014. You can find some links to papers about FRP on the GitHub page of reactnet. Coincidentally, I started my work on reactor around the same time that James Reeves started to work on reagi, and shortly after that Rich Hickey published core.async.

I applied FRP to enterprise-style UI forms in a very direct manner: Each visual component of a UI toolkit (e.g. a window, textfield, button, table, ...) provides mutable properties that allow to register listeners. This concept is strikingly similar to what FRP "behaviours" do in practice. So I adapted these properties of visual components to conform to reactor's Signal protocol (which represents a "behaviour"). Button presses or input-focus changes can likewise be seen as Eventsources, so consequently I registered numerous listeners that feed into individual Eventsource instances. This finally enabled me to represent all mutable data and events in the unified terms of FRP.

Although I experienced some relief from the resulting uniformity, I eventually got stuck, because my FRP implementation didn't seem to offer a natural way to express the interaction between UI forms. Similarly, I didn't see a way to handle long-running actions without resorting to some kind of callback mechanism. Perhaps my FRP implementation was simply missing the important Inversion of Control feature core.async provides. To summarize: I consider this experiment as a learning exercise, but not as a result to base future work on.

Update 31.10.2014: As it turned out, reactor was missing the flatmap combinator. I discovered this very recently while approaching GUI form interaction with my new reactor re-implementation. So, it is possible to solve this problem by using an FRP library that offers to dynamically create and connect new eventsources, which is essentially what a flatmap does with a passed-in function.

A Process-and-Channel oriented approach

Inspired by David Nolens article about CSP in ClojureScript and my recent learnings about how UI is managed in 55 years old IBM RPG I switched with my current async-ui prototype over to CSP and core.async. Here's a picture giving a rough overview of the design:

The rich client consists of processes and the state of the toolkit. The term "toolkit" refers for example to JavaFX or Swing. A process is a core.async go block. There are two predominant types of processes: A view is a map that contains a specification of the visual component tree, the domain data, a mapping between both, validation rules and validation results. This data represents the state of a UI form. In addition each view has its own events channel and references the root of a toolkits visual component tree (a JavaFX Stage or a Swing JFrame instance). For each view, one process is started, which processes events for that view. "Event processing" means that the data contained in the event is merged into the view state, validation is applied, an individual handler is invoked and the resulting view is passed via the central channel to the toolkit oriented process.

An event is a map created by toolkit specific event listeners that write them into the events channel of the corresponding view.

The concrete UI toolkit like JavaFX or Swing is kept behind the toolkit protocol. It is mainly implemented by a Builder and a Binding. The Builder takes the specification of a form and translates it to JavaFX or Swing API calls in order to create an actual visual component tree. The Binding registers listeners that put an event onto the views own events channel and creates setters that update the visual components properties with the data contained in the view.

An example

The example I have prepared so far shows a little master-detail scenario. Here's how the master form looks like when run with JavaFX:

To start the view process the following expression suffices:
(v/run-view #'item-manager-view
            #'item-manager-handler
            {:item ""
             :items ["Foo" "Bar" "Baz"]})
      
You can see that two function-vars are referenced, one points to the factory function that creates the initial data that represents the view. The other one points to the event handler function.

The visual component tree and the mapping is expressed by the factory function:
(defn item-manager-view
  [data]
  (let [spec
        (window "Item Manager"
                :content
                (panel "Content" :lygeneral "wrap 2, fill"
                                 :lycolumns "[|100,grow]" 
                                 :lyrows "[|200,grow|]"
                       :components
                       [(label "Item") (textfield "item" :lyhint "growx")
                        (listbox "items" :lyhint "span, grow")
                        (panel "Actions" :lygeneral "ins 0" :lyhint "span, right"
                               :components
                               [(button "Add Item")
                                (button "Edit Item")
                                (button "Remove Item")])]))]
    (-> (v/make-view "item-manager" spec)
        (assoc :mapping (v/make-mapping :item ["item" :text]
                                        :items ["items" :items]
                                        :selection ["items" :selection])
               :data data))))
      

The event handler is another Clojure function, and by means of the go block an asynchronous process:
(defn item-manager-handler
  [view event]
  (go (assoc view
        :data
        (let [data (:data view)]
          (case ((juxt :source :type) event)
            ["Add Item" :action]
            (-> data
                (update-in [:items] conj (:item data))
                (assoc :item ""))
            ["Edit Item" :action]
            (let [index (or (first (:selection data)) -1)]
              (if (not= index -1)
                (let [items (:items data)
                      editor-view (<! (v/run-view #'item-editor-view
                                                  #'item-editor-handler
                                                  {:text (nth items index)}))]
                  (if-not (:cancelled editor-view)
                    (assoc data
                      :items (replace-at items index [(-> editor-view :data :text)]))
                    data))
                data))
            ["Remove Item" :action]
            (assoc data
              :items (let [items (:items data)
                           index (or (first (:selection data)) -1)]
                       (if (not= index -1)
                         (replace-at items index [])
                         items)))
            data)))))
      
The go block is needed here because the master may start another asynchronous view process for displaying and handling the detail view. Please note that the communication between master and detail is almost like an ordinary function invocation. The master view process is paused until the detail process finishes.

Benefits

As it turns out, the channels establish a strict separation between the toolkit oriented process, which handles synchronization of view state with the visual component tree, and the view oriented process, which applies events to the view state and pushes updates into the central toolkit channel. While the toolkit oriented process must deal with all the dirty details of the JavaFX or Swing API, and has to keep mutable state in sync with the view state, the view oriented process solely deals with data transformation based on events, which themselves are simple maps. This is an important property, because it allows the frontend developer to stick to Clojure core functions to get his work done.

Unit testing presentation logic becomes a no-brainer, since we can feed events into the views channel and must only assert that the view state changes as expected.

The interaction between different views uses ad-hoc channels as returned by a go expression. By immediately starting a blocking read, view processes can wait for each other without blocking the UI event thread.

The channel infrastructure can also be used for dealing with long-running actions like calling remote services, which can easily go either into their own future, or use a non-blocking callback based API. Upon availability of the results the process feeds these into the events channel of a view, which handles them like any other event.

Conclusion

While I still think that FRP allows for very elegant solutions in the area of graphics animation or gaming, its core concepts "behaviour" and "eventsource" seem not crucial for solving the wide-spread problems in enterprise GUIs. But I'd like to be proven wrong on this point.

Update 31.10.2014: After I found out how form interaction can be expressed by means of FRP combinators, I consider it as one viable option to create typical enterprise "forms-over-data" GUIs. However, it took quite some time to grok this. For me, a helpful key insight was that eventsources are the way how asynchronous processes convey their results.

The idea of asynchronous processes is better visible in CSP because it materializes as go blocks in the code, thus after I dissected the whole problem into separated processes which are only coupled loosely through channels that carry dumb data, things became much easier.

My doubt about whether it makes sense to create a full-blown Clojure library for JavaFX programming has grown. While I'm convinced that the ideas and the design I used (like separated processes connected via channels, view representation by pure data, an explicit builder and a binding) are a good foundation, I'm afraid that project specific requirements between different applications vary in numerous details. A library that provides the level of comfort I consider as necessary inevitably becomes a batteries-included framework with lots of assumptions and implicit behaviour. My experience with those frameworks in Java-land tells me that such a "claim of omnipotence" almost always leads to pain and horrendous work-arounds. I still have to make up my mind if there is any piece in this picture that can be extracted to form a useful library.

Meanwhile, please feel free to exploit these ideas and the code I pushed to my GitHub repo.