I've implemented a Game of Life in Clojure and would like to understand what I can do better, especially in terms of idiomatic Clojure (without losing readability/maintainability) of the current version.
This is my first "serious" Clojure program (beyond hello world).
Things that I'm specifically interested in:
- Anything that's not idiomatic and thus should be changed/improved.
- Whether P (Point) should be a record (
(defrecord Point [x y]) (defn P [x y] (Point. x y))
) instead.
(ns com.nelkinda.game-of-life)
(def rules {
:survival #{2 3}
:birth #{3}
})
(defn P [x y] {:x x :y y})
(defn neighbors [cell]
(def neighbors-of-origin #{(P -1 -1) (P -1 0) (P -1 1) (P 0 -1) (P 0 1) (P 1 -1) (P 1 0) (P 1 1)})
(defn plus [cell1 cell2] {:x (+ (:x cell1) (:x cell2)) :y (+ (:y cell1) (:y cell2))})
(map #(plus cell %) neighbors-of-origin))
(defn next-generation [life]
(defn is-alive [cell] (contains? life cell))
(defn is-dead [cell] (not (is-alive cell)))
(defn dead-neighbors [cell] (filter #(is-dead %) (neighbors cell)))
(defn live-neighbors [cell] (filter #(is-alive %) (neighbors cell)))
(defn count-live-neighbors [cell] (count (live-neighbors cell)))
(defn born [cell rules] (contains? (:birth rules) (count-live-neighbors cell)))
(defn survives [cell rules] (contains? (:survival rules) (count-live-neighbors cell)))
(defn dead-neighbors-of-living-cells [] (mapcat #(dead-neighbors %) life))
(defn surviving-cells [] (filter #(survives % rules) life))
(defn born-cells [] (filter #(born % rules) (dead-neighbors-of-living-cells)))
(into #{} (concat (surviving-cells) (born-cells))))
Test Implementation:
(use 'com.nelkinda.game-of-life)
(use 'clojure.test)
(def universe (atom #{}))
(defn parse
([spec] (into #{} (parse (clojure.string/split spec #"") 0 0 '())))
([spec x y set] (case (first spec)
"." (parse (rest spec) (+ x 1) y set)
"*" (parse (rest spec) (+ x 1) y (cons (P x y) set))
"\n" (parse (rest spec) 0 (+ y 1) set)
(nil "") set
(throw (AssertionError. (str "Wrong input: '" (first spec) "'"))))))
(Given #"the following universe:" [spec]
(reset! universe (parse spec)))
(Then #"the next generation MUST be:" [spec]
(assert (= (reset! universe (next-generation @universe)) (parse spec))))
Test Specification:
Feature: Conway's Game of Life
Rules of Conway's Game of Life
> The universe of the _Game of Life_ is an infinite, two-dimensional orthogonal grid of square cells.
> Each cell is in one of two possible states:
> * Alive aka populated
> * Dead aka unpopulated
>
> Every cell interacts with its eight neighbors.
> The neighbors are the cells that are horizontally, vertically, or diagonally adjacent.
> At each step in time, the following transitions occur:
> 1. Underpopulation: Any live cell with fewer than 2 live neighbors dies.
> 1. Survival: Any live cell with 2 or 3 live neighbors survives on to the next generation.
> 1. Overpopulation Any live cell with more than 3 live neighbors dies.
> 1. Reproduction (birth): Any dead cell with exactly 3 live neighbors becomes a live cell.
Scenario: Empty universe
Given the following universe:
"""
"""
Then the next generation MUST be:
"""
"""
Scenario: Single cell universe
Given the following universe:
"""
*
"""
Then the next generation MUST be:
"""
"""
Scenario: Block
Given the following universe:
"""
**
**
"""
Then the next generation MUST be:
"""
**
**
"""
Scenario: Blinker
Given the following universe:
"""
.*
.*
.*
"""
Then the next generation MUST be:
"""
***
"""
Then the next generation MUST be:
"""
.*
.*
.*
"""
Scenario: Glider
Given the following universe:
"""
.*
..*
***
"""
Then the next generation MUST be:
"""
*.*
.**
.*
"""
Then the next generation MUST be:
"""
..*
*.*
.**
"""
Then the next generation MUST be:
"""
.*
..**
.**
"""
Then the next generation MUST be:
"""
..*
...*
.***
"""
Repository URL in case you want to clone it yourself: https://github.com/nelkinda/gameoflife-clojure
defn
is incorrect.defn
usually is used top level because it defines a function in the namesace scope. Each execution ofnext-generation
redefines the bunch of utility functions. If you put them top level they will be defined only once. I suggest you start with that refactoring. \$\endgroup\$