I'm a Clojure n00b (but am experienced with other languages) and am learning the language as I find time - enjoying it so far, though the strange dreams and accidental use of parenthesis elsewhere (e.g. Google searches) are a bit disconcerting!
Usually when learning a new language the first thing I do is implement Conway's Game of Life, since it's just complex enough to give a good sense of the language, but small enough in scope to be able to whip up in a couple of hours (most of which is spent wrestling with syntax). I did this with Scala a while back, and really appreciated the comments I received.
Anyhoo, having gone through this exercise with Clojure I was hoping to get similar feedback on my efforts so far?
I'm after any kind of feedback - algorithmic improvements, stylistic improvements, alternative APIs or language constructs, disgust at my insistence on checking argument types - whatever feedback you've got, I'm keen to hear it!
;
; Copyright (c) 2012 Peter Monks ([email protected])
;
; This work is licensed under the Creative Commons Attribution-ShareAlike
; 3.0 Unported License. To view a copy of this license, visit
; http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to
; Creative Commons, 444 Castro Street, Suite 900, Mountain View, California,
; 94041, USA.
;
(ns ClojureGameOfLife.core
(:use clojure.set))
(defrecord Cell [^Integer x ^Integer y])
(def block #{(Cell. 0 0) (Cell. 1 0)
(Cell. 0 1) (Cell. 1 1)})
(def beehive #{ (Cell. 1 0) (Cell. 2 0)
(Cell. 0 1) (Cell. 3 1)
(Cell. 1 2) (Cell. 2 2)})
(def blinker #{(Cell. 0 0)
(Cell. 0 1)
(Cell. 0 2)})
(def glider #{ (Cell. 1 0)
(Cell. 2 1)
(Cell. 0 2) (Cell. 1 2) (Cell. 2 2)})
(defn flat-map
"Maps the function to the given set, returning a new set (flattened, in the case where f itself
returns a set for each element).
Note: There has to be a better way to do this..."
[f s]
{:pre [(set? s)]
:post [(set? %)]}
(->> s (map f) (map seq) flatten set))
(defn alive?
"Is the given cell alive in the given generation?"
[generation
cell]
{:pre [(set? generation)
(instance? Cell cell)]
:post [(instance? Boolean %)]}
(contains? generation cell))
(defn neighbours
"Returns all of the neighbours of a given cell."
[cell]
{:pre [(instance? Cell cell)]
:post [(set? %)]}
(let [x (:x cell)
y (:y cell)]
#{
(Cell. (dec x) (dec y)) (Cell. x (dec y)) (Cell. (inc x) (dec y))
(Cell. (dec x) y) (Cell. (inc x) y)
(Cell. (dec x) (inc y)) (Cell. x (inc y)) (Cell. (inc x) (inc y))
}))
(defn number-of-living-neighbours
"Returns the number of living neighbours of the given cell in the given generation."
[generation
cell]
{:pre [(set? generation)
(instance? Cell cell)]
:post [(instance? Integer %)]}
(count (intersection generation (neighbours cell))))
(defn should-live?
"Should the given cell live in the next generation?"
[generation
cell]
{:pre [(set? generation)
(instance? Cell cell)]
:post [(instance? Boolean %)]}
(let [alive (alive? generation cell)
living-neighbours (number-of-living-neighbours generation cell)]
(if alive
(or (= 2 living-neighbours) (= 3 living-neighbours))
(= 3 living-neighbours))))
(defn next-generation
"Returns the next-generation board, given a generation."
[generation]
{:pre [(set? generation)]
:post [(set? %)]}
(set (filter #(should-live? generation %) (flat-map neighbours generation))))
(defn values
"The values of the given generation's given dimension."
[generation
elem]
{:pre [(set? generation)]
:post [(set? %)]}
(set (map elem generation)))
(defn x-values
"The X values of the given generation."
[generation]
{:pre [(set? generation)]
:post [(set? %)]}
(values generation :x))
(defn y-values
"The Y values of the given generation."
[generation]
{:pre [(set? generation)]
:post [(set? %)]}
(values generation :y))
(defn min-x
"Minimum X value of a generation."
[generation]
{:pre [(set? generation)]}
(reduce min (x-values generation)))
(defn min-y
"Minimum Y value of a generation."
[generation]
{:pre [(set? generation)]}
(reduce min (y-values generation)))
(defn max-x
"Maximum X value of a generation."
[generation]
{:pre [(set? generation)]}
(reduce max (x-values generation)))
(defn max-y
"Maximum Y value of a generation."
[generation]
{:pre [(set? generation)]}
(reduce max (y-values generation)))
(defn extend-by-1
"Extends a collection of numbers by 1 in each direction (both up and down)."
[c]
(let [minimum (reduce min c)
maximum (reduce max c)]
(conj c (dec minimum) (inc maximum))))
(defn print-generation
"Prints a generation."
[generation]
{:pre [(set? generation)]}
(doseq [y (sort (extend-by-1 (y-values generation)))]
(doseq [x (sort (extend-by-1 (x-values generation)))]
(if (alive? generation (Cell. x y))
(print "# ")
(print ". ")))
(println)))
(defn clear-screen
"Clears the screen, using ANSI control characters."
[]
(let [esc (char 27)]
(print (str esc "[2J")) ; ANSI: clear screen
(print (str esc "[;H")))) ; ANSI: move cursor to top left corner of screen
(defn -main
"Run Conway's Game of Life."
[& args]
(loop [saved-generations #{}
generation glider]
(if (not (contains? saved-generations generation))
(do
(clear-screen)
(print-generation generation)
(flush)
(Thread/sleep 250)
(recur (conj saved-generations generation) (next-generation generation))))))