clojureconways-game-of-life

Conway's Game of Life in Clojure: Add Glider to Grid


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)))
  1. new-grid creates a grid.
  2. get-coordinates turns the grid with true/false states into a grid of row/col coordinates.
  3. 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:

  1. What does that error message mean?
  2. Is my approach using apply sensible, or how should one tackle such an issue?

Solution

  • 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)))