Dienstag, 19. März 2013

STM mit Bankkonten

Clojure zeichnet sich durch ein besonderes Feature aus. Dieses Feature ist die STM (Software Transactional Memory). Die STM kann wie eine transaktionsgesteuerte In Memory Datenbank gesehen werden. Wie bei einer Datenbank ist es auch hier möglich, die Zugriffe auf Variablen innerhalb einer Transaktion laufen zu lassen. Die STM sorgt dafür, dass  bei Änderungen innerhalb einer Transaktion keine inkosistenten Datenkonstelationen entstehen. Lesezugriffe anderer  Funktionen sehen keine "halben" Änderungen der Daten.

Clojure bietet insgesamt 4 Möglichkeiten mit asynchronen Szenarien umzugehen.
  1. atom: Das Szenario für ein atom ist eine synchrone unabhängige Veränderung.
  2. ref: Das Szenario für refs sind synchrone abhängige Veränderungen.
  3. agent: Das Szenario für agents sind asynchrone unabhängige Veränderungen.
  4. vars: Spielen eine (meiner Meinung nach) Sonderrolle und sind für isolierte Veränderungen zu gebrauchen.
Der Blog Beitrag handelt nur die Option 2, die refs ab. Im nachfolgenden Szenario werden refs dafür verwendet um Bankkonten zu verwalten. Bei diesen Bankkonten sollen Beträge zwischen den Konten hin und her bewegt werden. Desweiteren werden in einer weiteren Liste die Aktivitäten zu den einzelnen Kontobewegungen mit protokolliert. Das spannende an dieser Aufgabe ist es, alle diese Aktivitäten sicher und ohne Kollisionen hin zu bekommen.

Aber schauen wir dazu lieber auf ein wenig Clojure Code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
(ns account)

(use 'clojure.pprint)

;simpel record definition for an account
(defrecord Account [nr name ammount])

; a list for logging events
(def account-logs (ref ()))

(defn clear-account-logs []
  (dosync
    (ref-set account-logs ())))

(defn- base-transfer
  "Make a money transfer from to with an amount."
  [from to ammount]
  (alter from update-in [:ammount] - ammount)
  (alter to update-in [:ammount] + ammount))

(defn- base-transfer-log
  "Logs a money transfer."
  [from to ammount]
  (alter account-logs conj (str System.DateTime/Now " " ammount " " (:name @ from) "->" (:name @ to))))

(defn- create-safe-transfer [transfer-fn log-fn]
  (fn [from to ammount]
    (dosync
      (transfer-fn from to ammount)
      (log-fn from to ammount))))

; construct a transfer method
(def transfer (create-safe-transfer base-transfer base-transfer-log))

(defn many-transfers-sleep
  "Makes a money transfer from to with an ammount.
   The transfer will executed times count. 
   Between each one there is a sleep time in mili seconds"
  [from to ammount times sleep]
  (dotimes [x times]
    (System.Threading.Thread/Sleep sleep)
    (transfer from to ammount)
    ))

(defn status [& accounts]
  (let [accounts (apply vector accounts)]
    (pprint (count @account-logs))
    (pprint accounts)))

In der Zeile 1 geben wir den Namespace unserer Datei an. Zeile 3 enthält eine use Anweisung für die Clojure pprint Funktionen. Diese benutzen wir später um die Kontostände auszugeben.

In Zeile 6 definieren wir uns einen Record, der die Kontonummer(nr), Namen(name) und Kontostand(ammount) aufnimmt. Es geht bei dem Sample ja um die Demonstration der STM und nicht um echte Kontenbewegungen.

In Zeile 9 kommen wir das erste mal mit einen ref Datentype in Berührung. Hier wird die Variable account-logs angelegt. Der Initiale Wert dieser Referenz ist eine leere Liste. Achtung: "-" Zeichen in Clojure Variablen und Funktionsnamen sind erlaubt und durchaus üblich.

In Zeile 11 startet die Definition der Funktion clear-account-logs. Wichtig hier ist, dass die Manipulation eines Ref Typen nur innerhalb einer dosync Anweisung erfolgen darf. Versuche der Manipulation außerhalb einer dosync Anweisung sind nicht zulässig und werden von Clojure nicht ausgeführt. Mit der ref-set Funktion wird die account-logs Liste wieder auf eine leere Liste gesetzt.

In Zeile 15 beginnt die Funktion base-transfer. Die ersten beiden Parameter ("from" und "to") von base-transfer sind refs von Typen Account. "ammount" ist der Betrag der von den "from" Konto abgebucht und zum "to" Konto zu gebucht wird. Die Manipulation der refs hier erfolgt mit der alter Funktion. In der ersten alter Anweisung wird das "from" Konto ärmer (- ammount) und das "to" Konto reicher (+ ammount).

Zeile 21 enthält eine Funktion base-transfer-log. Die Funktion hat die selben Parameter wie base-transfer, aber hier wird nur die log Variable account-logs um einen Satz ergänzt. Dieses erfolgt in der Zeile 24 mit Hilfe der alter Funktion. Hier wird mit der conj Funktion die Liste entsprechend um eine Zeile pro Aufruf ergänzt. Das Format der Log Zeile wird mit der str Funktion zusammen gebaut.

Zeile 26 ist ein wenig tricky. Hierbei handelt es sich um einen Closure. Die Funktion create-safe-transfer hat als Rückgabe Wert eine neue Funktion. Diese neue generierte Funktion hat die Übergabe Parameter "from", "to" und "ammount". Innerhalb dieser neuen Funktion werden die beiden übergebenen Funktionen transfer-fn und log-fn innerhalb eines dosync Blocks aufgerufen und verarbeiten dabei die Übergabeparameter "from", "to" und "ammount". Das ist jetzt schon richtig tiefe funktionale Programmierung. Wir haben eine Funktion, die Funktionen als Übergabeparameter erwartet und selbst wieder eine Funktion als Rückgabewert liefert.

Alle 3 vorangegangenen Funktionen wurden mit defn- implementiert. defn- erzeugt eine in Namespace private Funktion. Ich möchte nicht, dass die 3 erzeugten Funktionen nach außen für andere Namespaces sichtbar werden. Die Funktionen base-transfer und base-transfer-log würden bei einem direkten Aufruf auch nur Fehler erzeugen, da der umschließende dosync Block fehlt.

In Zeile 33 benutzen wir nun unsere create-safe-transfer Funktion und generieren uns die transfer Funktion aus den beiden übergebenen Funktionen base-transfer und base-transfer-log.

Das interessante an dieser create Funktion ist die Erweiterbarkeit an dieser Stelle. Wir könnten uns die Transfer Funktion auch aus vier anstatt aus zwei übergebenen Funktionen konstruieren lassen. Mögliche Szenarien wären das Protokollieren und Vermerken von Buchungen, die größer als 10000 sind, oder das Überprüfen und Protokollieren, ob der Kontostand ins Negative geht. Anstatt von 2 hätte die create Funktion dann 4 Aufruf Funktionen, die innerhalb des dosync Blocks aufgerufen würden. An dieser Stelle könnte die create Funktion auch generisch umgeschrieben werden. Aber für dieses Sample soll es erst einmal so einfach bleiben.

Es folgen jetzt noch zwei weitere Funktionen, die nur dazu gebraucht werden um die STM auf Herz und Nieren zu prüfen.

Die Funktion many-transfers-sleep steuert funktional nichts mehr zur API bei, aber wird später bei den Sample dazu genutz, um Last auf das System zu geben. Diese Funktion hat die 3 üblichen Parameter "from", "to" und "ammount". Der vierte Parameter "times" bestimmt die Anzahl einer dotimes Blocks. "sleep" ist eine Wartezeit in Millsekunden zwischen den Aufrufen. Wir können jetzt einen Transfer hundert oder tausend mal wiederholen lassen.

Die letzte Funktion status in Zeile 45 gibt über die pprint Funktion die übergeben Accounts und die Anzahl der Logliste aus.

So, jetzt gilt es das ganze an der REPL auszuprobieren und zu testen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
; use for our three used function
(use '[account :only (many-transfers-sleep clear-account-logs status)])

; two sample accounts
(def account1 (ref (account.Account. 1 "Thomas" 2000.0)))
(def account2 (ref (account.Account. 2 "Andrea" 500.0)))

; transfer 100 bucks 10 times and sleep 1000 mseconds between every transfer
(many-transfers-sleep account1 account2 100 10 1000)

;transfers via future
(clear-account-logs)

(future (many-transfers-sleep account1 account2 25 200 50))
(future (many-transfers-sleep account2 account1 31 200 60))
(future (many-transfers-sleep account1 account2 17 200 70))
(future (many-transfers-sleep account1 account2 8 200 80))
(future (many-transfers-sleep account2 account1 19 200 90))

(status @account1 @account2)

Wir können jetzt Stück für Stück unsere Funktion ausprobieren.

Mit der use Anweisung in Zeile 2 geben wir die 3 benutzen Funktionen bekannt.

In Zeile 5 und 6 legen wir uns zwei Test Konten an, um die Tests damit durchführen zu können.

In Zeile 9 haben wir einen exemplarischen Aufruf der Funktion many-transfers-sleep. Es werden von account1 nach account2 100 übertragen. Das ganze 10 mal mit jeweils 1000 Millisekunden (also 1 Sekunde) Pause. Das Ergebnis dieser Zeile ist leider ein wenig ernüchternd. Wenn diese Zeile an der REPL ausgeführt wird, wird many-transfers-sleep synchron auf den REPL Thread ausgeführt. Die REPL bleibt "hängen" und kommt nach 10 Sekunden wieder. OK, dafür brauchen wir keine STM.

Da müssen wir den Test also anders aufziehen. Um die STM wirklich auf Herz und Nieren zu prüfen, bedienen wir uns des future von Clojure. future ist in diesem Fall ein Makro, was die innenliegende Anweisung auf einen anderen Thread ausführt.

Schauen wir dazu exemplarisch auf die Zeile 14. Hier wird der Aufruf der Funktion many-transfers-sleep innerhalb des future ausgeführt. Wenn diese Zeile an der REPL ausprobiert wird, dann steht der REPL Thread sofort wieder zur Verfügung, so dass wir die nächste Anweisung tippen könnten. Die Anweisung innerhalb des future wird auf einen anderen Thread ausgeführt. Das ist jetzt schon interessanter.

Also, um das Ganze richtig zu testen, führen wir jetzt die Zeile 12 - 18 am Block aus. Was passiert hier? In Zeile 12 wird unsere Log Liste gelöscht. In den Zeilen 14 - 18 werden verschiedene Aufrufe von many-transfers-sleep asynchron abgesetzt. In den Zeilen 14,16 und 17 werden jeweils Beträge von account1 nach account2 transferiert. Diese sind 25, 17 und 8 (in Summe 50). In den Zeilen 15 und 18 geht es dann in die andere Richtung mit jeweils 31 und 19 (in Summe 50). Jeder dieser Transfers wird dann 200 mal ausgeführt. Da die transferierten Summen jeweils 50 hin wie zurück sind, ist das Ganze ein Nullsummen Spiel. Um die ganze Sache noch ein wenig unberechenbarer zu machen, sind die Wartezeiten (der letzte Parameter) ein wenig unterschiedlich. Nach dem Abschluss aller Transfers müssen die beiden Konten auf dem gleichen Stand sein wie vor dem Start der Anweisungen. Die Anzahl der Logeinträge muss genau 1000 sein.

Mit dem Aufruf der Status Funktion in Zeile 20 kann der Stand der Dinge jeweils abgefragt werden.
Also die Zeilen 12 bis 18 in die REPL pasten und ausführen. Dann alle 2 Sekunden die status Funktion ausführen, um zu beobachten, wie die Dinge sich entwickeln.



Jetzt kann natürlich experimentiert und getestet werden.
  • Änderung der Beträge
  • Änderung der Transferzeiten.
  • Änderung der Transferanzahl
  • Weitere Konten
  • u.s.w

Zusammenfassung

Ich für meinen Teil habe mit diesem Code rumgespielt, und versucht die STM auszuhebeln. Mir ist es nicht gelungen. Alle berechneten Werte waren absolut korrekt. Es ist schon beindruckend, wie einfach das arbeiten mit refs ist. Einfach den dosync Block um die Anweisung herum und das war's. Es sind genau solche Funktionalitäten, die Clojure zu einer eindrucksvollen Sprache machen.

Ich habe auf einen Github Eintrag verzichtet. Der komplette Quellcode ist innerhalb dieses Postings vorhanden.















Sonntag, 4. November 2012

Game of Life

Ich habe vor ca. 8 Monaten meine Liebe zu Clojure gefunden. Clojure hat seinen Ursprung ja auf der JVM, aber es gibt auch eine Portierung auf der CLR. Da ich nun mal ein .NET Entwickler bin, liegt mir diese Implementierung nun mal mehr am Herzen.

Ich habe also mehrere Bücher zu Clojure bestellt und durchgearbeitet. Während dieser Experimente habe ich die REPL natürlich gequält und einiges am Code eingegeben. Nun war es aber mal an der Zeit ein richtiges Programm zu schreiben. Es ist zwar halt nur ein kleines, aber an diesem Sample können einige Wirkmechanismen und Ideen von Clojure gezeigt werden.

Für mein erstes Clojure Programm habe ich mir eine einfache Implementierung von "Conways Game of Life" ausgesucht. Für die Spielregeln bitte nach googeln. Das Programm ist einfach gestrickt und hat keine Extras. Diese Oberfläche wurde mit Windows Forms realisiert. In nachfolgendem Bild ist das laufende Programm zu sehen.


Mit Start Game kann das Spiel gestartet/gestoppt werden. In der Textbox wird die laufende Iteration angezeigt. Mit dem New Game Button wird ein neues Spiel gestartet. Per Zufall wird das Board dann gefüllt.

Ich habe das Programm in  Visual Studio mit Hilfe des Clojure Plugins (vsClojure) realisiert. (Kann über den Extension Manager von Visual Studio installiert werden)

In Visual Studio ergibt sich dann folgendes Bild.

 
Das Programm besteht aus den Dateien Board.clj. In Board.clj ist die Spiellogik enthalten. GameOfLife.clj enthält die Windows Forms UI. BoardTest.clj enthält die Unit Test für das Board.
 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
(ns Board)

;; board handling
;; independent of ui
(defn- make-row 
  "Makes a sequence of length which is randomly filled with :s or :u"
  [length]
  (for [x (range length)]
    (if (< (rand 10) 5)
      :s
      :u)))

(defn make-board 
  "Makes a board as square of length with :s :u randomly set"
  [length]
  (for [x (range length)]
    (make-row length)))

(defn- neighbour-count 
  "Calculates the count of neighbours with :s of x/y in board"
  [board x y]
  (let  [length (count board)
        r1 (nth board (- y 1) (repeat length :u))
        r2 (nth board y)
        r3  (nth board (+ y 1) (repeat length :u))
        r11 [(nth r1 (- x 1) :u) (nth r1 x) (nth r1 (+ x 1) :u)]
        r21 [(nth r2 (- x 1) :u) :u         (nth r2 (+ x 1) :u)]
        r31 [(nth r3 (- x 1) :u) (nth r3 x) (nth r3 (+ x 1) :u)]
        ]
    (count (filter #(= % :s)(concat r11 r21 r31)))))

(defn next-board 
  "Calculates the next board regarding conway law's"
  [board]
  (let [length (count board)]
    (for [y (range length)]
      (let [row (nth board y)]
        (for [x (range length)]
          (let [count (neighbour-count board x y)]
            (if (= (nth row x) :s)
              (if (or (= count 2) (= count 3))
                :s
                :u)
              (if (= count 3)
                :s
                :u))))))))

Also fangen wir von oben an.

make-row erstellt eine Sequence mit der Länge length  die zufallsmäßig mit :s :u gefüllt ist. :s steht hierbei für eine gesetzte Zelle :u für eine ungesetzte. Die Funktion ist private definiert, da sie nur hier gebraucht wird.

make-board ruft length mal make-row auf. Wir haben dann eine Sequence der Länge length, deren Elemente jeweils eine Sequence der Länge length ist. Die innere Sequence ist mit :s oder :u zufallsmäßig besetzt. Wir haben also ein "quadratisches", Zufall besetztes Board mit den wir beginnen können.

neighbour-count berechnet die Anzahl der gesetzten Nachbarzellen an der Koordinate x/y innerhalb des Boards. Die meiste Logik befindet sich innerhalb der let Anweisung. Als erstes bestimmen wir alle benötigten Zeilen des Boards. r2 ist an einfachsten. Wir holen uns mit der nth Funktion einfach die y-te Zeile. r1 kann genauso berechnet werden, aber leider haben wir für y = 0 einen Sonderfall, da wir keine Zeile mit den Index -1 haben. Unser Spielboard ist halt begrenzt. Die nth Funktion hat dafür aber eine passende Überladung. Der 4-te Parameter ist in diesen Fall ein Default Wert, der dann zurückgeliefert wird wenn der Index von nth sozusagen ins Leere greift. Derselbe Fall ist bei r3, dort haben wir den Fall, das y + 1 nicht vorhanden sein kann. Auch dort geben wir für diesen Sonderfall eine Zeile die nur mit :u gesetzt wird zurück.  Nach der Separtion der Zeilen müssen wir nun die Spalten ermitteln. Hier bedienen wir uns derselben Fähigkeiten von nth. Wir holen uns aus der Zeile die Spalten (x -1), x und  (x+1) heraus. Wenn der obere oder untere Rand des Index erreicht ist, dann liefern wir als Default :u zurück. Ein weiterer Sonderfall ist das Element x/y selber. Dies ist ja der Punkt selber. Damit beim späteren Zusammenrechnen kein Fehler auftaucht, wird dieser Punkt konstant auf :u gesetzt. (Zu sehen in der mittleren Zeile von r21). In den Body der let Anweisung selber concatenieren wir die drei kleinen, jeweils aus drei Elementen bestehenden Einzelsequencen zusammen und filtern das Ergebnis nach Elementen mit :s. Das Ergebnis wird dann nur noch zusammen gezählt (count) und als Rückgabe von neighbour-count zurückgeliefert. Die Funktion selber ist eine Hilfsfunktion deren Scope privat ist.

next-board berechnet aus den übergebenen Start Board die nächste Iteration. In der ersten let Anweisung ermitteln wir die Länge des Boards. (Bitte dran denken, das Board ist quadratisch, so dass wir die Länge für die x und auch y Richtung benutzen können). Darauf folgt das erste for das y von 0 bis length - 1 durchiteriert. Die nächste let Anweisung bindet in row die benutzte Zeile. Die nächste for Anweisung lässt x von 0 bis lenght - 1 laufen. Die letzte let Anweisung bindet an count die Anzahl der gesetzten Nachbar Zellen bezüglich der x/y Koordinate. Die nachfolgende if Anweisung bestimmt, ob die aktuelle Zelle gesetzt ist. Wenn ja und die Anzahl der gesetzten Nachbarn ist 2 oder 3, dann bleibt diese Zelle weiterhin gesetzt. Wenn nicht, dann haucht Sie leider Ihr Leben aus und bekommt den Wert :u. War die Zelle an x/y nicht am Leben ist (sprich mit :u belegt) und die Anzahl der lebenden Nachbar ist genau 3, dann entsteht mit :s ein neues Leben. Wenn nicht bleibt es bei :u.

Das ganze kann dann schon mal an der REPLausprobiert werden.



Als nächstes werfen wir einem Blick auf die UI die mit Windows Forms implementiert ist. Die UI befindet sich in der Datei GameOfLife.clj.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
(ns GameOfLife
  (:gen-class))

(use '[Board :only (make-board next-board)])

;; UI Handling for game of life
;; Windows forms technologie
(System.Reflection.Assembly/LoadWithPartialName "System.Windows.Forms")

(import (System.Drawing 
          Size Point Color SolidBrush))
(import (System.Windows.Forms
          Form TableLayoutPanel Label Button TextBox Panel Timer PaintEventHandler PaintEventArgs))

(defn- paint 
  "Paints the board to the panel"
  [pnl board]
  (let [length (count board)
        black (SolidBrush. Color/Black)
        white (SolidBrush. Color/White)
        w (int (/ (.get_Width pnl) length))
        h (int (/ (.get_Height pnl) length))
        g (.CreateGraphics pnl) ]
    (doseq [y (range length)
            x (range length)]
      (let [row (nth board y)
            cell (nth row x)]
      (if (= cell :s)
        (.FillRectangle g black (* x w) (* y h) w h)
        (.FillRectangle g white (* x w) (* y h) w h)
        ))
    )
    (.Dispose black)
    (.Dispose white)
    (.Dispose g)
    ))

(defn game-of-life 
  "Main program for starting the game with a square of length"
  [length]
   (let [form (Form.)
         pnl-paint (Panel.)
         btnNewGame (Button.)
         btnStartStop (Button.)
         txtCount (TextBox.)
         gameTimer (Timer.)
         gameCount (atom 1)
         board (atom (make-board length))]

     (doto btnNewGame
       (.set_Text "New Game")
       (.set_Size (Size. 100 25))
       (.set_Location (Point. 1 10)))

     (doto btnStartStop
       (.set_Tag "IsStoped")
       (.set_Text "Start Game")
       (.set_Size (Size. 100 25))
       (.set_Location (Point. 120 10)))

     (doto txtCount
       (.set_Size (Size. 100 25))
       (.set_Location (Point. 240 10)))

     (doto gameTimer
       (.set_Interval 500))

     (doto pnl-paint
       (.set_Size (Size. 287 219))
       (.set_Location (Point. 1 42))
       (.set_Anchor 15)
       (.set_BackColor Color/White))
         
     (.. form Controls (Add btnNewGame))
     (.. form Controls (Add btnStartStop))
     (.. form Controls (Add txtCount))
     (.. form Controls (Add pnl-paint))

     (.add_Tick gameTimer
       (gen-delegate EventHandler [sender args]
         (do
           (.set_Text txtCount (str @gameCount))
           (paint pnl-paint @board)
           (swap! gameCount inc)
           (reset! board (next-board @board))
           )))
       
    (.add_Click btnNewGame
      (gen-delegate EventHandler [sender args]
          (do
            (reset! board (make-board length))
            (reset! gameCount 1))))

     (.add_Click btnStartStop
       (gen-delegate EventHandler [sender args]
         (if (= (.get_Tag btnStartStop) "IsStoped")
           (do
             (.set_Tag btnStartStop "IsStarted")
             (.set_Text btnStartStop "Stop Game")
             (.Start gameTimer))
           (do
             (.set_Tag btnStartStop "IsStoped")
             (.set_Text btnStartStop "Start Game")
             (.Stop gameTimer)))))
                                   
      (doto form             
        (.set_Text "Game of Life")
        (.set_Size (Size. 300 300))
        .ShowDialog)))

(defn -main 
  "Main Entry Point, starts the game with a board lengh of 50"
  [& args]
  (game-of-life 50))

Die Datei fängt mit einer use Anweisung an. Hier werden die beiden Funktionen make-board und next-board zur Verwendung bekannt gegeben.
Mit der LoadWithPartialName Funktion wird das System.Windows.Forms Assembly geladen. Die nachfolgenden Import Anweisungen geben die Klassen bekannt, die innerhalb dieser Datei verwendet werden. Z.B Size, Point, Color und SolidBrush aus den System.Drawing Namespace.

paint ist eine Hilfsfunktion mit lokalen Scope, die ein board auf dem übergebenen Panel zeichnet. In der let Anweisung werden schwarze und weiße Pinsel erzeugt, Breiten und Höhen berechnet, sowie das Graphics Objekt erzeugt. In den Body des let werden alle y und x Kombinationen bearbeitet. Ist die Zelle an der Position x/y gesetzt, dann wird schwarz gezeichnet, ansonsten weiß. Nach der Schleife sind wir noch so nett, und geben die unmanged Ressourcen schon mal freiwillig zurück.

Die game-of-life funktion ist das Hauptprogramm selber. Der übergebene Parameter length bestimmt die Maße des Spiels. In der startenden let Anweisung werden die Windows Forms Elemente erzeugt. Des Weiteren werden 2 Atome angelegt die den State des Spiels beinhalten. gameCount ist ein Integer mit der Anzahl der Iterationen. Der Startwert hier ist 1. board ist das Spiel selber. Initial wird ein neues board mit make-board erzeugt. In den nachfolgen doto Anweisungen werden die UI Elemente zusammen gebaut. Besonders erwähnenswert hier ist vielleicht das Timer Intervall von 500 ( 1/2 Sekunde) des gameTimer. (Achtung: Clojure arbeitet mit den nativen Funktionsnamen. Die Property Size, die in Visual Studio Projekten (C#/VB) auch so benutzt werden kann, wird unter der Haube mit get_Size und set_Size übersetzt.) .
Nachfolgend werden die Event Handler implementiert. add_Tick des game timers ist der Erste. Der Handler wird aller 500 Millisekunden durchlaufen. Innerhalb des Handlers wird der gameCount in die Textbox geschrieben. Die paint Funktion zeichnet dann das Board neu. Der gameCount wird erhöht und die nächste Version des Board wird berechnet und auf das Atom board zurückgeschrieben. Der Spielkreislauf ist damit komplett.
Der Click Eventhandler des btnNewGame Button erzeugt ein neues Board und resetet das atom board. Der gameCount wird wieder auf 1 gesetzt.
Der Click EventHandler des btnStartStop Buttons startet/stoppt den gameTimer. Der Button Text wird entsprechend geändert. Die Steuerung des Buttons wird über die Tag Property realisiert.
In letzten Teil wird die Form mit Parameter versehen und mit Show Dialog angezeigt.

Die main Funktion startet das das game-of-life mit der Board length 50.

Die dritte Clojure Datei ist BoardTest.clj. Die Datei enthält einige Unit Test die das Board entsprechend ab testen. Exemplarisch ein Test der den "Blinker" ab testet. Der Blinker ist ein Stab aus drei nebeneinander gesetzten Zellen. Entsprechend der Game of Life Regeln wird der Blinker in der nächsten Iteration hoch stehend aus drei übereinander gesetzten Zellen bestehen. Mit der nächsten Iteration wiederholt sich das Ganze.


1
2
3
4
5
6
7
8
9
(deftest blinker-test
  (let [b [[:u :u :u] 
           [:s :s :s]
           [:u :u :u]]]
    (is (= [[:u :s :u] 
            [:u :s :u]
            [:u :s :u]] (next-board b)))
    (is (= b (next-board (next-board b))))  
    ))

Der erste Test stellt sicher, dass der Blinker gedreht ist. Der zweite Test (2 Aufrufe von next-board) muss identisch mit der Ausgangssituation sein.

Nachtrag: Es gibt an diesen Programm sicherlich noch jede Menge Verbesserungs Möglichkeiten. Grade in der Funktion neighbour-count ist jede Menge Redundanz drin. Ich vermute mal die Funktion next-board kann auch kompackter formuliert werden. Bei der UI fehlt die Behandlung des Paint Events. Nach einen Resize der UI ist das panel leer. Da das Board aber alle 1/2 Sekunde neu gezeichnet wird, habe ich darauf verzeichtet.

Die verwendete Clojure Version ist 1.3.0. Der Quellcode kann von Github runtergeladen werden.
http://github.com/thomasschulte/clojureclr-gol-wf