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: EventStream
s and Signal
s:
EventStream[T]
is a collection of values of typeT
happening at certain moments in timeSignal[T]
is a value of typeT
which 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
filter
it to get only the events you're interested in - you can
map
the events to other events - you can
flatMap
with a function returning anotherEventStream
and 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
EventStream
source or of anEventStream
sink (sometimes both)those components explicitely declare the abstract type of streamed events they're expecting. For a given
TextField
this might be something likeNewSelectedFile
whether the selection comes from aFileChooser
or from a simpleTextField
the 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 EventStream
s:
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:
TotalWordsNumbers
uses the fullResults
object 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)ReferencesTable
just needs the referencesErrorMessageBox
needs the error message so we justmap
that
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
inBackground
mechanism is enhanced with an additionalactionProgress
eventStream 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
Trigger
forEventStream[()]
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
Observing
trait. 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
doCount
event 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 | countAction
creating a file poller is easy using the
Timer
class 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
doCount
becomes: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.