I'm trying to implement the Game of Life in Clojure. I managed to implement the main logic, but now I'd like to provide a couple of functions that add an object (e.g. a Glider) to the grid.
Here are some functions that work fine so far:
(ns glider.core
(:gen-class))
(defn new-grid
"Creates a 2d grid with rows*cols."
[rows cols]
(partition cols (take (* rows cols) (repeat false))))
(defn get-coordinates
"Returns the row/col coordinates of each 2d grid field."
[grid]
(let [rows (count grid)
cols (count (get grid 0))]
(partition cols (for [r (range rows) c (range cols)] [r c]))))
(defn set-at
"Sets the field at row/col to state."
[grid row col state]
(assoc grid row (assoc (get grid row) col state)))
new-grid
creates a grid.get-coordinates
turns the grid with true
/false
states into a grid of row/col coordinates.set-at
sets a new state to the grid at row/col.Now I'd like to add a glider. Here's my approach:
(defn add-glider
"Adds a glider pattern to the lower-right quadrant of the grid."
[grid]
(let [rows (count grid)
cols (count (get grid 0))
row-offset (/ rows 2)
col-offset (/ cols 2)
glider [[false true false]
[false false true]
[true true true]]
glider-coords (get-coordinates glider)
grid-coords (map (fn [[r c]] [(+ r row-offset) (+ c col-offset)]) glider-coords)]
(apply (fn [[r c]] (set-at grid r c true)) grid-coords)))
First, I compute rows
/cols
, and also the mid-point of the grid (row-offset
/col-offset
). Second, I build up a glider
in a 3x3 field with true
/false
states. I turn the glider into coordinates (glider-coords
), which I then move by the offset (grid-coords
).
Now I have a sequence of coordinates. I'd like to set the grid state to true
on all these points.
I wrote this program to test it:
(defn -main
"Creates a grid and adds a glider to it."
[& args]
(let [grid (new-grid 8 8)
grid-with-glider (add-glider grid)]
(println grid)
(println grid-with-glider)))
Which gives me the following error message when I run it using lein
:
$ lein repl
glider.core=> (-main)
Execution error (ClassCastException) at glider.core/add-glider$fn (core.clj:32).
class clojure.lang.PersistentVector cannot be cast to class java.lang.Number (clojure.lang.PersistentVector is in unnamed module of loader 'app'; java.lang.Number is in module java.base of loader 'bootstrap')
Line 32
refers to the following code:
grid-coords (map (fn [[r c]] [(+ r row-offset) (+ c col-offset)]) glider-coords)
My questions:
apply
sensible, or how should one tackle such an issue?The error means that a Number is expected, but an array is provided. Why that happens, we can see if we capture the value of glider-coords
, for example by doing the following while debugging:
defn add-glider
"Adds a glider pattern to the lower-right quadrant of the grid."
[grid]
(let [rows (count grid)
cols (count (get grid 0))
row-offset (/ rows 2)
col-offset (/ cols 2)
glider [[false true false]
[false false true]
[true true true]]
glider-coords (get-coordinates glider)
_ (def glider-coords-debug glider-coords)
grid-coords (map (fn [[r c]] [(+ r row-offset) (+ c col-offset)]) glider-coords)]
(apply (fn [[r c]] (set-at grid r c true)) grid-coords)))
Now, if you look at the contents, you will see:
> glider-coords-debug
(
([0 0] [0 1] [0 2])
([1 0] [1 1] [1 2])
([2 0] [2 1] [2 2])
)
And doing a similar trick on the next, reveals the value of r
:
(map (fn [[r c]] [(+ r row-offset) (+ c col-offset)]) glider-coords)
In the first iteration, the value of r
is [0 0]
, which is not a Number, as +
expects, and that is the cause of the error message.
Regarding the apply
there, it seems very wrong. I would rewrite that piece using reduce
+ assoc-in
, as the idea is to iteratively modify the state by traversing the results. You even don't have to destructure the coordinates vector. Something like
(reduce #(assoc-in %1 %2 true)
grid
grid-coords))
There's also an error with get-coordinates
- you should return only those indices you want to update (= these, where (get-in grid [r c])
is true
).
Here is a fixed version:
(ns glider.core
(:gen-class))
(defn new-grid
"Creates a 2d grid with rows*cols."
[rows cols]
;; makes sure its a vector of vectors, to get random access using assoc
(mapv (fn [_] (into [] (repeat rows false)))
(range cols)))
(defn get-coordinates
"Returns the row/col coordinates of each 2d grid field."
[grid]
(let [rows (count grid)
cols (count (grid 0))]
(for [r (range rows)
c (range cols)
:when (get-in grid [r c])]
[r c])))
(defn add-glider
"Adds a glider pattern to the lower-right quadrant of the grid."
[grid]
(let [rows (count grid)
cols (count (grid 0))
row-offset (/ rows 2)
col-offset (/ cols 2)
glider [[false true false]
[false false true]
[true true true]]
grid-coords (->> (get-coordinates glider)
(map (fn [[r c]] [(+ r row-offset) (+ c col-offset)])))]
(reduce #(assoc-in %1 %2 true)
grid
grid-coords)))
(defn -main
"Creates a grid and adds a glider to it."
[& args]
(let [grid (new-grid 8 8)
grid-with-glider (add-glider grid)]
(println grid-with-glider)))