Langton's ant in Clojurescript The weekly coding challenge in Eric Normand's newsletter this week involves implementing Langton's ant.

I wanted something on my screen so I put together a simple re-frame app. The core functions relating to the ant:

world.cljs

(ns ant.world)

(defn init-world [width height]
  {:width width
   :height height
   :grid (into [] (repeat height (into [] (repeat width 0))))
   :ant-pos [(quot width 2) (quot height 2)]
   :ant-dir :up
   :stopped? false})

(defn within-bounds? [[row col] width height]
  (and (&gt;= row 0) (&gt;= col 0)
       (&lt; row width) (&lt; col height)))

(defn turn-right [dir]
  (case dir
    :up :right
    :right :down
    :down :left
    :left :up))

(defn turn-left [dir]
  (case dir
    :up :left
    :left :down
    :down :right
    :right :up))

(defn move [[x y] dir]
  (case dir
    :up    [x (dec y)]
    :right [(inc x) y]
    :down  [x (inc y)]
    :left  [(dec x) y]))

(defn iter-world [{:keys [width height grid
                          ant-pos
                          ant-dir
                          stopped?]
                   :as world}]
  (if (and (not stopped?) (within-bounds? ant-pos width height))
    (let [cell-color (get-in grid ant-pos)
          is-white? (= cell-color 0)
          next-dir (if is-white? (turn-right ant-dir) (turn-left ant-dir))
          next-pos (move ant-pos next-dir)
          next-grid (assoc-in grid ant-pos (if is-white? 1 0))]
      {:width width
       :height height
       :grid next-grid
       :ant-pos next-pos
       :ant-dir next-dir
       :stopped? false})
    (assoc world :stopped? true)))


And the view:

views.cljs

(ns ant.views
  (:require
   [re-frame.core :as re-frame]
   [ant.subs :as subs]
   [ant.events :as ev]
   [cljs.pprint :refer [pprint]]
   ))

(defn main-panel []
  (let [grid (re-frame/subscribe [::subs/grid])]
    (into [:table {:style {:border "1px solid red"}}]
          (for [row @grid]
            (into [:tr]
                  (for [cell row]
                    [:td {:width "5px" :height "5px"
                          :style {:backgroundColor (if (= cell 1) "black" "white")}}]))))))

(defn dispatch-timer-event
  []
  (let [now (js/Date.)]
    (re-frame/dispatch [::ev/timer now])))  ;; &lt;-- dispatch used

;; Call the dispatching function every second.
;; `defonce` is like `def` but it ensures only one instance is ever
;; created in the face of figwheel hot-reloading of this file.
(defonce do-timer (js/setInterval dispatch-timer-event 50))


subscriptions:

subs.cljs

(ns ant.subs
  (:require
   [re-frame.core :as re-frame]))

(re-frame/reg-sub
 ::grid
 (fn [db]
   (:grid db)))


Events:

events.cljs

(ns ant.events
  (:require
   [re-frame.core :as re-frame]
   [ant.db :as db]
   [ant.world :as aw]
   ))

(re-frame/reg-event-db
 ::initialize-db
 (fn [_ _]
   db/default-db))

(re-frame/reg-event-db
 ::timer
 (fn [db _]
   (aw/iter-world db)))


DB:

db.cljs


(ns ant.db
  (:require [ant.world :as aw]))

(def default-db
  (aw/init-world 100 100))


This works. But is quite slow. Though the timer is supposed to trigger an event every 50ms, the drawing rate is not so high.

What can I do to improve the performance here? Is it possible without resorting to mutable data structures?

(Any general code improvement suggestions are also most welcome)

Full code is here:
https://github.com/nakiya/langton-ant
I don't have any performance related suggestions unfortunately. I do have a couple cleanup suggestions though.

The bounds checking function can be cleaned up a bit by making use of "comparison chaining" similar to what you'd use in Pytbon:

(defn within-bounds? [[row col] width height]
  (and (&lt; -1 row width)
       (&lt; -1 col height)))




In the iteration function, you have

{:width width
 :height height
 :grid next-grid
 :ant-pos next-pos
 :ant-dir next-dir
 :stopped? false}


This is less than ideal for a few reasons


You're reassociating entries that already exist in the map. This is unlikely to effect performance, but causes unneeded code bloat. It also creates a potential source of bugs if you add a new entry into the game state. Will you remember to update the map retuned by the function? I'd use assoc here instead.
You're associating false with the :stopped? key. Following the logic of the function though, it must already be falsey there.


I'd just write

(assoc world :grid next-grid
             :ant-pos next-pos
             :ant-dir next-dir}




In init-world, you're using (into []) to convert to vectors. This can be written slightly neater using vec.

:grid (vec (repeat height
                   (vec (repeat width 0))))


I also typically space this code out like I have above (I actually write this exact code fairly often).



This is entirely subjective, but instead of maintaining a :stopped? flag, I prefer to use a :running? one instead (the logic is flipped). I find it tends to lead to easier to read code.