Some of the reaction for Waterfront was related to the Application
Context Pattern (ACP) - The pattern that allows most of Waterfront's
code to be purely functional. I'll try to explain the basics in this
post. Let me start with the motivation: the reason why FP is at odds
with GUI code.
(Pure) Functional code has no side effects, which implies immutability
of state. There are no fields nor global variables that can be
assigned to. Thus, one function can affect the computation carried out
by another function only by the passing of parameters. Most GUI
systems are built around the notion of event handlers which are
invoked by a message processing loop. There is no chain of calls from
one event handler to another.
In particular, if handler "A" computed some new value it cannot pass
it on to handler "B" because the system will call "B" only after "A"
returns. That's the predicament.
ACP overcomes this by capturing the applications current state in an
immutable map. All event handlers receive a single parameter which is
the "current" context and compute the "new" context. A typical handler
(henceforth: "context processing function") will carry out these
activities: (a) Examine the current context; (b) Perform some GUI
operations (setSize, setText, etc.); (c) Compute a new context based
on the current context and on information obtained from the GUI
(getText, etc.). The caller (henceforth: "dispatcher") takes the
returned context and will use it as the new current context, the next
time a context processing function is invoked.
This means that when you register event handler with a Swing widget
the handler needs to to call the ACP dispatcher passing it a context
processing function.
The net effect of this approach is that only the dispatcher has to
deal with mutable state. The context processors are functional: they
merely compute the new state from the current.
application-context-pattern.clj (
http://groups.google.com/group/
clojure/web/application-context-pattern.clj) shows a concrete example.
It's about 140 LOC (ripped off from the real Waterfront codebase)
structured as follows:
Lines 1..40: General-purpose helpers.
Lines 40..90: The ACP infrastructure
Lines 90..140: A quick sample, built around ACP.
The sample program opens a JFrame with two buttons: Input and Output.
A click on the input button will pop-up an input dialog box. A click
on the output button will pop-up a message box showing the last value
entered into the input box. There's also a JLabel showing the length
of the input, but let's ignore it for the moment.
The entry point into the ACP world is the bootstrap function. It takes
two parameters: a context processing function and an initial context.
In the example, this is carried out at the bottom of the run-it
function:
(defn run-it []
(let [build-ui (fn [ctx]
(let [f (javax.swing.JFrame. "Frame")
b-in (javax.swing.JButton. "Input")
b-out (javax.swing.JButton. "Output")]
(.addActionListener b-in (new-action-listener (fn [event]
((ctx :dispatch) get-input))))
(.addActionListener b-out (new-action-listener (fn [event]
((ctx :dispatch) show-output))))
(.setLayout f (java.awt.FlowLayout.))
(doseq [x [b-in b-out]]
(.add f x) )
(doto f
(.setSize 500 300)
(.setDefaultCloseOperation javax.swing.JFrame/
DISPOSE_ON_CLOSE)
(.setVisible true))
(assoc ctx :frame f) ))]
(invoke-later #(bootstrap build-ui {})) ))
invoke-later is a utility function that is mapped to SwingUtilities/
invokeLater.
Let's drill down into the build-ui function: It takes the current
context (ctx parameter). Then it creates the frame and the buttons. It
uses new-action-listener (another utility) to register an action
listener with the buttons. The first listener looks like this:
((ctx :dispatch) get-input))))
It uses (ctx :dispatch) to obtain the dispatcher from which ctx was
obtained, and evaluates it passing get-input as the context processing
function. The call to bootstrap initialized this dispatcher and added
the :dispatch mapping to the initial context.
get-input looks like this:
(defn- get-input [ctx]
(let [reply (javax.swing.JOptionPane/showInputDialog nil "Type in
something")]
(assoc ctx :user-input reply) ))
It pops-up an input box, and returns a new context which is the same
as the current context except that :user-input is now mapped to value
returned from the input box.
show-output is the context processing function for the output button:
(defn- show-output [ctx]
(javax.swing.JOptionPane/showMessageDialog nil (ctx :user-
input)) )
Note that show-output returns nil which the dispatcher interprets as
"no change to ctx".
What we have is that get-input communicates with show-output by
returning a new context. There's no assignment into atoms or the
likes. The mutable state is encapsulated within the dispatcher.
It is now time for the JLabel to step into the plate. We now want to
add a JLabel that will show the length of the user input. We want this
label to be updated on the fly, with no explicit user request. This
type of behavior is common in GUI applications. To this end, the
dispatcher also supports the notion of observers. In ACP an observer
is a function takes two contexts: old-ctx and new-ctx.
An observer typically compares the contexts. If the mappings it is
interested in were changed, it carries out these activities:
(a) Updates the GUI (setText, setEnabled, etc.); (b) Computes a new
context (that is: a context that is even newer than new-ctx). After a
context processing function was invoked, the dispatcher will run all
observers in a loop until a steady state was reached.
Here's the observer that takes care of updating the label:
(defn- input-observer [old-ctx new-ctx]
(when-not (= (old-ctx :user-input) (new-ctx :user-input))
(.setText (new-ctx :label) (str "Input length: " (count (new-
ctx :user-input)))) ))
One register an observer by adding it to (ctx :observers):
(assoc ctx :frame f :label label :observers (cons input-
observer (ctx :observers)))
There all sort of variations on these theme and some subtleties, but I
want to keep this post coherent so I'll skip these for now. Anyway,
all the fundamentals are here. As you can see this pattern is quite
powerful. Almost all functionality of Waterfront is built on top of
this.
For instance, the menu system is described as a DSL (a vector of maps)
mapped to the :menu key of the context. An observer keeps an eye on
the :menu mapping and rebuilds the JMenuBar whenever it changes. Also,
the plugin-loader is an observer that watches the :plugins list. When
a new entry is found, it loads this plugin. In fact, in Waterfront,
the startup function simply registers the plugin observer and loads
the list of plugins from a file.
(file:
http://groups.google.com/group/clojure/web/application-context-pattern.clj)