4
\$\begingroup\$

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 (>= row 0) (>= col 0)
       (< row width) (< 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])))  ;; <-- 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

\$\endgroup\$
3
\$\begingroup\$

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 (< -1 row width)
       (< -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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.