Pages

12 October 2011

Counting words - part 2

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 type T happening at certain moments in time
  • Signal[T] is a value of type T 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 another EventStream 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 an EventStream sink (sometimes both)

  • those components explicitely declare the abstract type of streamed events they're expecting. For a given TextField this might be something like NewSelectedFile whether the selection comes from a FileChooser or from a simple TextField

  • 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 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:

  • TotalWordsNumbers uses the full Results 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 references

  • ErrorMessageBox needs the error message so we just map 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 additional actionProgress 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 time

  • I found useful to introduce a trait called Trigger for EventStream[()]

  • I also added an EventStreamSourceProxy[T] extends EventStream[T] trait which can be mixed in any GUI class. This trait uses an internal EventSource[T] which is the implementation of the EventStream[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 new shinyMethod (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:

  1. 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 action

    val doCount: EventStream[Any] = filePath.distinct.change | countAction
    
  2. creating a file poller is easy using the Timer class from the reactive library

    class 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.
    
  3. 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.

2 comments:

jed said...

Mate, great stuff! I love that the improved functionality just fell out of the architecture.

Miguel Negrão said...

Great article ! I've been looking for ages for a concrete example of swing + FRP, I'm so glad I finally found it !