Make sure you read through Lau's original post first, and understand the code there.
In a functional language like Clojure, it is easy to experiment with using different structures to represent the same data. Rather than being hidden in a rat's nest of mutable object relationships, your data is right in front of you in simple persistent data structures. In Lau's implementation, the board is represented by a list of lists, like so:
Each cell in the list knows its state (
:dying), and its x and y coordinates on the board. The board data structure is used for two purposes:
stepfunction applies the rules of the automaton, returning the board's next state
renderfunction draws the board on a Swing panel
These two functions have slightly different needs: the
step function cares only about the state of adjacent cells, and can ignore the coordinates, while the
render function needs both.
How hard would it be to convert the data to a form that stores only the state? Not hard at all:
You could write
without-coords as a one-liner using
map, but I prefer how the nested
fors visually call out the fact that you are manipulating two dimentional data.
Without the coords, the board is easier to read:
If you choose to store the board this way, you will need to get the coordinates back for rendering. That's easy too, again using nested
fors to demonstrate that you are transforming two-dimensional data:
So, with a couple of tiny functions, you can easily convert between two different representations of the data. Why not use both formats, picking the right one for each function's needs?
When performance is critical, there is another advantage to using different data formats: caching. Consider the
step function, which uses the
rules function to determine the next value of each cell:
The "without coordinates" format used by
rules passes only exactly the data needed. As a result, the universe of legal inputs to
rules is small enough to fit in a small cache in memory. And in-memory caching is trivial in Clojure, simply call
memoize on a function. (It turns out that for this particular example, the calculation is simple enough that
memoize won't buy you anything. Lau's second post demonstrates more useful optimizations: transients and double-buffering. But in some problems a cacheable function result is a performance lifesaver.)
If you use comprehensions such as Clojure's
for to convert inputs to exactly the data a function needs, your functions will be simpler to read and write. This "caller makes right" approach is not always appropriate. When it is appropriate, it is far less tedious to implement than the related adapter pattern from OO programming.
Since multiple data formats are so easy, you can use yet another format for testing.
Brian's Brain is a simulation in two dimensions, it would be nice to write tests with a literal, visual, 2-d representation. In other words:
In the literal form above the
O is an
:on cell, and the
. is an
Creating this representation is easy. The
board->str function converts a board to a compact string form:
board->chars helper is equally simple:
With the new stringified board format, you can trivially write tests like this:
are macro makes it simple to run the same tests over multiple inputs, and with liberal use of whitespace the tests line up visually. It isn't perfect, but I think it is good enough.
One last note: the string format used in tests is basically ASCII art, so you can have a console based GUI almost for free:
Ok, JMX integration is gratuitous for an example like this. But clojure.contrib.jmx is so easy to use I couldn't resist. You can store the total number of iterations performed in a thread-safe Clojure atom:
Then, just expose the atom as a JMX mbean.
Yes, it is that easy. Create any Clojure reference type, point it at a map, and register a bean. You can now access the iteration counter from a JMX client such as the jconsole application that ships with the JDK.
To make the mbean report real data, wrap the automaton's iterations in an
update-stage helper function that both does the work, and updates the counter.
If you haven't seen
update-in (and its cousins
assoc-in) before, go and study them for a moment now. They make working with non-trivial data structures a joy.
You might disagree with my choice of atoms. With a pair of references, you could keep the iteration count exactly coordinated with the simulator. Or, with a reference plus an agent you push the work of updating the iteration count out of the main loop. Whatever you choose, Clojure makes it easy to both (a) implement state and (b) keep the statefulness separate from the bulk of your code.
Lau's original code weighed in at a trim 67 lines. Now that the app supports three different data formats, a console UI, and JMX integration, it is up to around 150 lines. How should we organize such a monster of an app? Two obvious choices are:
I don't love either approach. The single file approach is confusing for the reader, because there are multiple different things going on. The multiple namespace approach is a pain for callers, because they get weighed down under a bunch of namespaces to do a single thing.
A third option is
immigrate you can organize your code into multiple namespaces for the benefit of readers, and then immigrate them all into a blanket namespace for casual users of the API. But immigrate may be too cute for their own good.
Instead, I chose to use one namespace for the convenience of callers, and mutilple files to provide sub-namespace organization for readers of code. I mimiced the structure Tom Faulhaber used in clojure-contrib's pprint library: a top level file that calls
load on several files in a subdirectory of the same name (minus the .clj extension):
lau/brians_brain.clj lau/brians_brain/automaton.clj lau/brians_brain/board.clj lau/brians_brain/console_gui.clj lau/brians_brain/swing_gui.clj
I also used this layout for clojure-contrib's JMX library.
Over the course of Lau's two exampples and this one, you have seen:
And here are some things you haven't seen:
Would it be possible to write a threadsafe Brian's Brain using mutable OO? Of course. Is there a benefit to doing so? I would love to hear your thoughts on the subject, especially in the form of code.