In this small post, I'm going to show how I used Functional Reactive Programming (FRP) to improve the GUI part of the small application presented in the previous post.
The best article to read on the subject is Deprecating the Observer Pattern (DOP). This article explains what are the drawbacks of using a listener-based approach to components communication in a user interface and proposes FRP as an alternative.
Be reactive
What I've been using is a simplified yet powerful version of the code described in DOP, using the Reactive library by Naftoli Gugenheim. In this library you have 2 essential concepts: EventStreams and Signals:
EventStream[T]is a collection of values of typeThappening at certain moments in timeSignal[T]is a value of typeTwhich can change from time to time
They are actually 2 sides of the same coin (more or less) as explained in this video: from an EventStream you can get a Signal by holding the last emitted value and from a Signal you can get an EventStream by getting all the changes.
The great thing about an EventStream (let's focus on that one for now) is that you can manipulate it like a collection of objects:
- you can
filterit to get only the events you're interested in - you can
mapthe events to other events - you can
flatMapwith a function returning anotherEventStreamand so on,...
But how is this helpful for GUI programming?
With GUI components
The hard reality of Swing GUI components is that they are really mutable at heart. Once you compose a GUI with components (say a Frame) containing other components (say a TextField), then, when anything happens in your application, you mutate the innermost components heavily (by changing the text color for example). When you add publish/subscribe mechanisms on top of that, you add even more developer-managed mutability since you need to add-remove listeners to the whole graph of components. It is also not very easy to understand how events "flow" between components.
The way I see the usage of FRP with GUIs is:
components are seen as either creating values to propagate (like a button action) or consuming values (like a text field change with the new value). Hence they have a role of an
EventStreamsource or of anEventStreamsink (sometimes both)those components explicitely declare the abstract type of streamed events they're expecting. For a given
TextFieldthis might be something likeNewSelectedFilewhether the selection comes from aFileChooseror from a simpleTextFieldthe event streams can be merged, filtered, mapped functionally, with no side-effect so that the logic of events routing is very composable and explicit
Let's see that more precisely in the context of my simple application.
An example
In my WordCount application, the first thing the user does is to select a file to count. Then she is supposed to click on the "Count" button to start the count before the results are displayed. In terms of graphical components I have:
val openFileMenuItem = OpenFileMenuItem("./src/test/resources")
val countAction = CountAction()
What you don't see above is that those 2 custom GUI components have been declared as extending EventStream[T] (simplified code for the explanation):
OpenFileMenuItem ... with EventStream[File]
CountAction ... with EventStream[Unit]
This means that when you open a file using the OpenFileMenuItem you're providing new File events which other components can react on, and when you invoke the CountAction you, well,... you just pressed the button, there's no meaningful value to convey, so the Unit type is appropriate here (the clients just want to know that something happened).
Then we can compose those 2 EventStreams:
val filePath: Signal[String] = openFileMenuItem.map(_.getPath).hold("")
val doCount: EventStream[Any] = filePath.distinct.change | countAction
First we do a bit of filtering, because we just need the file path as a Signal[String] (using hold to transform the stream to a signal, with an empty initial value).
Then we declare that we need to do a count whether there's a distinct change in the file path value, or (|), if the user pressed countAction button.
How do we "consume" this doCount stream of events? We flatMap it to another stream of events providing the results of the counting:
val results: EventStream[Results] =
doCount flatMap { doIt => WordCounter.count(filePath.now).inBackground }
For each doCount event we flatMap it (actually we forget about it,...) and we use the current value of the filePath to count the number of words.
The expression computeValue.inBackground computes a value using the SwingUtils.invokeLater method to avoid computations being done on the event dispatch thread (this might cause grey screens). The inBackground method returns an EventStream[T] to signal consumers that the value is ready.
Since the result of counting is an EventStream[Results] I can then "plug" it into the GUI component doing the display:
val resultsPanel = ResultsPanel(results)
And that's it. The ResultsPanel component doesn't care where the values come from, who created them. It is also interesting to see how the ResultsPanel declares its sub-components:
object ResultsPanel {
def apply(results: EventStream[Results]) = {
new ResultsPanel(TotalWordsNumbers(results),
ReferencesTable(results.map(_.references)),
ErrorMessageBox(results.map(_.message)))
}
}
There are 3 sub-components and they use different parts of the Results event stream, so we use the map function to restrict exactly the stream to what is needed:
TotalWordsNumbersuses the fullResultsobject to display the total words count (the one we really want), the references word count and the full text word count (to check that the count is ok)ReferencesTablejust needs the referencesErrorMessageBoxneeds the error message so we justmapthat
Finally, how is the event stream used in the component itself? Let's look at the ErrorMessageBox component:
case class ErrorMessageBox(message: EventStream[String]) extends TextField with Observing {
foreground = Color.red
font = new Font("Courier", Font.PLAIN, 15);
editable = false
message.foreach { m => text = m }
}
The important line is the last one where we declare that for each message event, we change the text attribute of the TextField to the new value m.
Some implementation notes
If you read the actual code, you'll find quite a few differences with the real implementation:
the
inBackgroundmechanism is enhanced with an additionalactionProgresseventStream which fires events before and after the action to execute. This is used to change the main frame cursor to a waiting watch when the computation takes some timeI found useful to introduce a trait called
TriggerforEventStream[()]I also added an
EventStreamSourceProxy[T] extends EventStream[T]trait which can be mixed in any GUI class. This trait uses an internalEventSource[T]which is the implementation of theEventStream[T]and can be used to fire events. For example:// When the action is executed (a file is selected) we fire an event and // the whole `OpenFileMenuItem` component acts as an `EventStream[File]`. case class OpenFileMenuItem(start: String) extends MenuItem("Open") with EventStreamSourceProxy[File] { ... action = new Action("Open") { def apply() { ... source.fire(fileChooser.selectedFile) } } }I had some difficult debugging time with the
Observingtrait. If you place it on an object that's going to be garbage collected, your components will not be notified of new events. This happened to me as I placed it on a class used for an implicit conversion, in order to get a newshinyMethod(tm):implicit def toComponent(a: A): ImplicitComponent = new ImplicitComponent(a) class ImplicitComponent(a: A) extends Observing { def shinyMethod = a }
Features ideas
This is something which amazed me: just having to think about event streams made me rethink the application functionalities. How? When I started thinking about what would trigger a word count I realized that:
there is no reason why the user should have to click the "Count" button when the file is selected, we can do the count and display the results right away
thinking about event stream as a flux made me realize that the file can be polled regularly for changes and the results displayed whenever there's a change
Changing my program to incorporate those 2 ideas was soooo easy:
the first idea is reflected by the
doCountevent stream definition given above, we just say that we want to count whenever there's a file selection or a count actionval doCount: EventStream[Any] = filePath.distinct.change | countActioncreating a file poller is easy using the
Timerclass from the reactive libraryclass FilePoller(path: Signal[String], delay: Long = 500) extends Trigger { private var previousLastModified = new File(path.now).lastModified() val timer = new Timer(0, delay, {t => false}) foreach { tick => def newLastModified = new File(path.now).lastModified() if (newLastModified > previousLastModified || newLastModified == 0) { previousLastModified = newLastModified source.fire(()) } } } The `FilePoller` uses a `path` signal regularly. If the underlying file is modified, a notification event is triggered.then the final version of
doCountbecomes:lazy val filePoller = new FilePoller(filePath) lazy val doCount: EventStream[Any] = filePath.distinct.change | countAction | filePoller
I don't know about you but I really find nice that the abstractions in my implementation give me hints about what the application could do! For me this is a good sign that FRP is really well-suited for the job of GUI programming.
2 comments:
Mate, great stuff! I love that the improved functionality just fell out of the architecture.
Great article ! I've been looking for ages for a concrete example of swing + FRP, I'm so glad I finally found it !
Post a Comment