Here's the changes from the previous version. Full code on my Git repository.
(defstruct point :x :y)
(defn world-at [world point]
(get world point))
(defn toggle-pos [world point]
(if (zero? (world-at world point))
(assoc world point 1)
(assoc world point 0)))
(defn neighbours [p]
(let [x (:x p) y (:y p)]
[(struct point (dec x) (dec y)) (struct point x (dec y)) (struct point (inc x) (dec y))
(struct point (dec x) y) (struct point (inc x) y)
(struct point (dec x) (inc y)) (struct point x (inc y)) (struct point (inc x) (inc y))]))
(defn neighbour-count [world p]
(reduce + (map (fn [x] (let [v (world-at world x)] (if (nil? v) 0 v))) (neighbours p))))
(defn new-state [world p]
(let [neighbours (neighbour-count world p) alive (world-at world p)]
(and (= alive 1) (< neighbours 2)) 0 ;; under population
(and (= alive 1) (> neighbours 3)) 0 ;; over-crowding
(and (= alive 1) (or (= 2 neighbours) (= 3 neighbours))) 1 ;; unchanged to the next generation
(and (= 3 neighbours)) 1 ;; any tile with exactly 3 live neighbour cells becomes alive
(defn life-step [w]
(into (hash-map) (map (fn [x] [(first x) (new-state w (first x))]) w)))
(defn create-world [w h]
(let [x (range 0 w) y (range 0 h)]
(apply hash-map (mapcat (fn [a] (mapcat (fn [b] (list (struct point a b) 0)) y)) x))))
Most of the functions above are much clearer (apart from
create-world) than they were previously. In addition the SLOC has decreased from 74 to 66, so the code is more concise too.
I did sacrifice sparseness for neatness. I needed to have the values populated as dead such that
life-stepcould just be written as a map function. If the values didn't exist, I'd have to create something from nothing. In this case, I think the trade off is OK.
Overall, I think this is a definite improvement over the previous version.