maandag 10 september 2012

Game of Clojure Life

Last weekend was the 2012 edition of the Devnology weekend. One of my goals for this weekend was to write my first non-trivial Lisp/Clojure app. I had been willing to give that a try for a long time, but the interest renewed after the Clojure session of last Wednesday.

Since I didn't spot anyone else interested in giving it a try, I decided to spend some time to do it on my own, but with some guided Lisp advice of Michel and the moral support of many other Devnologists. And moral 'support you need'; trying to build something in Clojure. Not only because the task at hand was a bit overambitious, but also because the tooling is far from mature (or deterministic, which is an aspect I find very important :) )

The target for me was to create a visual Game of Life implementation so it was actually a combination of multiple earlier devnology events.

The approach used was :

  • Think of a new function and documented it + rationale
  • Define the function in my clojure file,
  • Update the REPL,
  • Test it, and the test usually ended up as the body of the new method
  • ( I did not write unit-tests, but the REPL is really good for testing, after a REPL-session I should have saved them as tests for regression though)

    I think you could say it was somewhat inspired by literate programming (writing the logic down with their rationale) and TDD (all code in the clojure file started as test)

    As a result of this procedure; the following code is more or less the transcript of my coding session. Also almost all of the code is written in this chronological order. Top of the file being the oldest of course. Only the failed JavaFX part has been removed.

    (ns myLeinProject.gol)
    
    ; The first decision I made was to represent the field as a single list, although i wanted my algorithms
    ; to mimic a matrix, I didn't like having that complexity in my first application and mimicking a matrix on
    ; a single list is not that hard. Also i decided this would be easier than a sparse list of only live cells.
    ;
    ; later on i realized it might have been easier (or at least less code) to just write the neighbors
    ; algorithm in terms of a single list. Since the current approach has bidirectional mapping from coord to
    ; list-position and back
    ;
    ; we use > 1 values for living cells, 0 for dead ones. The number represents the number of alive iterations...
    ; (could resist the gold plating for keeping the 'age' )
    
    
    (def fieldTest
      " field with certain test patterns (formatted as a matrix but really just a list, therefor we need
        to define the height, width.
          contains: block, beehive, blinker, toad see http://en.wikipedia.org/wiki/Conway%27s_Game_of_Life "
      (list
        0 0 0 0 0 0 0 0 1 0
        0 0 0 0 0 0 1 0 0 1
        0 0 1 1 0 0 1 0 0 1
        0 0 1 1 0 0 0 1 0 0
        0 0 0 0 0 0 0 0 0 0
        0 0 0 0 0 0 0 0 0 0
        0 0 1 0 0 0 1 1 0 0
        0 0 1 0 0 1 0 0 1 0
        0 0 1 0 0 0 1 1 0 0
        0 0 0 0 0 0 0 0 0 0
        ))
    
    ; needs to be in line with the field dimensions, alternatively i could have just limited to square fields and us the root of the list-length instead of manually saving dimensions in globals
    (def getFieldWidth 10)
    (def getFieldHeigth 10)
    
    (defn printField [field]
      " helper method to print the list as matrix to the console"
      (doseq [line (partition getFieldWidth field)]
        (println line)
      )
      (println "")
    )
    
    (defn getPos [x,y]
      "calculates the list-position for the coordinate"
      (+ x (* y getFieldWidth ))
    )
    
    (defn getVal [ x, y, field]
     " we assume that all cells next to the grid are dead-cells, alternatively we could wrap around... "
      ( cond
        (< x 0) 0 ; left of the field
        (>= x getFieldWidth) 0 ; right of the field
        (< y 0) 0 ; above the field
        (>= y getFieldHeigth) 0 ; below the field
        :else (nth field (getPos x y))
      )
    )
    
    
    (defn getStatusVal[ x, y, field]
      " since we store the age in the list we use this method to just get a 1 for all living cells
        making it just a bit easier to calc neighbors (just sum the numbers)"
      ( if (> (getVal x y field) 0)
            1
            0
        )
    )
    
    
    (defn getNeighbourCells [x y field]
      " returns the surround cell-status for x,y. but excluding x,y itself"
      (list
            (getStatusVal (- x 1) (- y 1) field)
            (getStatusVal x (- y 1) field)
            (getStatusVal (+ x 1) (- y 1) field)
    
            (getStatusVal (- x 1) y field)
            ; (getMinimalVal x y field) self
            (getStatusVal (+ x 1) y field)
    
            (getStatusVal (- x 1) (+ y 1) field)
            (getStatusVal x (+ y 1) field)
            (getStatusVal (+ x 1) (+ y 1) field)
       )
    )
    
    (defn countNeighbours [ x, y, field]
      "counting the neighbours is just summing up the values (0,1) of them"
      (reduce + (getNeighbourCells x y field) )
    )
    
    ;Any live cell with fewer than two live neighbours dies, as if caused by under-population.
    ;Any live cell with two or three live neighbours lives on to the next generation.
    ;Any live cell with more than three live neighbours dies, as if by overcrowding.
    (defn shouldLive [ x, y, field]
       (and
             (> (getVal x y field) 0) ; is Alive ?
             (> (countNeighbours x, y, field) 1) ; under population
             (< (countNeighbours x, y, field) 4) ; over population
          )
    
    )
    
    ;Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
    (defn shouldComeAlive [ x, y, field]
      (= (countNeighbours x, y, field) 3)
    )
    
    (defn nextStatus [x, y, field]
        (cond
          ( shouldLive x, y, field ) (+ (getVal x y field) 1) ; raise its age
          ( shouldComeAlive x, y, field ) 1 ; resurrect
          :else 0 ; remain dead or die
         )
    )
    
    ;since I wanted to map over the list I needed something to translate the list position back to x,y
    (defn getX[x]
      ( mod x getFieldWidth )
    )
    
    (defn getY[x]
      ( quot x getFieldWidth )
    )
    
    (defn progress [field]
      "create a range with all list indexes of the field and then map them (with the old field) to new field values into a new field list"
      ( map #(nextStatus (getX %) (getY %) field) (range 0 (* getFieldWidth getFieldHeigth)) )
    )
    
    ; since using JavaFX2 didn;t work out, i decided to write a html file with a table to visualize the field
    ; letting the browser rendering my field-status
    ; html is really awful but works on my machine (which is enough for the proof of concept)
    
    ; shamelessly copied from a website how to write a file
    (import '(java.io BufferedWriter FileWriter))
    (defn write-lines [file-name lines]
      (with-open [wtr (BufferedWriter. (FileWriter. file-name))]
        (doseq [line lines] (.write wtr line))))
    
    
    
    (defn html [x field]
      "turn the cell into a html tag"
      (let [cellVal (getVal (getX x) (getY x) field)]
      (cond
        (> cellVal 5) ""; really old cells become blue
        (> cellVal 0) ""; living cells are green
        :else "" ;dead cells are grey
      )
      )
    )
    
    (defn htmlLine [x field]
      "adds a html row tag when needed"
      (if (= (getX x) 0) ; start of line
         (str "" ( html x field))
         ( html x field)
      )
    )
    
    (defn tohtml [field]
      "maps the field to html"
      (map #(htmlLine % field) (range 0 (* getFieldWidth getFieldHeigth)))
    )
    
    (defn transformAndWriteToHtmlFile [field]
      (write-lines "d:/gol.html" (concat (list "") (tohtml field) (list "
    "))) ) (defn play [round max field] "the master method, call with (play 0 fieldTest " (printField field) ; console output (transformAndWriteToHtmlFile field) ; file output (. Thread (sleep 5000)) ; wait (when (< round max) ; progress when needed (play (+ 1 round) max (progress field)) ) )

    Lessons learned:

    Mind bending language?

    Although Lisp is known for its (){}[] just adding them 'to be safe' is not a good choice :-) An extra pair of () just means something different, so they should be carefully placed. It's not like in Java math/logic expressions where you just can do either 3 * 4 * 2 or ((3 *4) *2) or (3 * ((4) *2)).
    It took me about an hour to adjust to automatically / subconsciously write expressions the clojure style (* 3 2) instead of (3 * 2), so although most people find the syntax & semantics weird (i.e. not C style like) it actually doesn't take that much time to get used to. But that does not make you a Clojure programmer just yet, or so I have learned :)

    Tools are not good enough for noobs like me

    Eclipse Juno and CounterClockwise are not really mature. I had lots of encounters with problems that could be solved by either re-reloading the source file to REPL. Or by just completely restarting Eclipse. This seriously complicates development in a unfamiliar language, you want deterministic behaviour to be sure you're doing it wrong, or right; but at least be able to draw a conclusion from the actions and results. Now, after carefully reading error messages, you just have to decide 'could this error really be from this code' and then if not, restart the environment, which in almost all situations, magically fixed it.
    And I'm not going to talk bad about the error messages themselves, but to put it mildly, they could use some improvement (especially since the type errors are only caught at runtime in a language like this).
    Also I missed tool support for extracting functions/constants.Instead, I used comments for somethings that I'd rather have replaced with a named-const or function.
    Another thing that surprised me is that function ordering seemed to matter, you may not call a function that is declared below the calling location. I'm not sure what the exact rules are but this did strike me as odd. I rather have the (manually) extracted one-line methods below the place they got extracted from (example: getX)

    Java integration can be hard

    Clojure and JavaFX don't really mix. I lost quite some time trying to integrate JavaFX 2.2 into my clojure app for visualizing the game. As a sort of knowledgeable JavaFX user, this should have been a piece of cake. Though it was not, the class structure and the use of static initializer magic of the framework was not conquerable with my limited clojure experience. Also it seems there isn't much help online either (and I didn't bring my 'JavaFX 2 in Action' book to look the details up). I know there are special clojure bindings but I was unable to find them and writing my own interop in Java didn't work out to well either. Hence I ended up with the formerly discussed HTML visualization.



    All things considered, I'm pretty happy with the results and doing this exercise did give me a much better view of what the language is really about.