We managed to make a rough version but we didn't have time to test it and make it nice.
When I got home I redid the exercise from scratch using a mix of TDD and REPL Driven Development.
First, I coded a couple of evolution rules. These are their tests:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns cellular-animation.rules-test | |
(:require | |
[cljs.test :refer-macros [deftest testing is]] | |
[cellular-animation.rules :as rules])) | |
(deftest rules-tests | |
(testing "rule 90" | |
;+-----------------------------------------------------------------+ | |
;| Neighborhood | 111 | 110 | 101 | 100 | 011 | 010 | 001 | 000 | | |
;+-----------------------------------------------------------------+ | |
;| New Center Cell | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 0 | | |
;+-----------------------------------------------------------------+ | |
(is (= (rules/rule-90 [1 1 1]) 0)) | |
(is (= (rules/rule-90 [1 1 0]) 1)) | |
(is (= (rules/rule-90 [1 0 1]) 0)) | |
(is (= (rules/rule-90 [1 0 0]) 1)) | |
(is (= (rules/rule-90 [0 1 1]) 1)) | |
(is (= (rules/rule-90 [0 1 0]) 0)) | |
(is (= (rules/rule-90 [0 0 1]) 1)) | |
(is (= (rules/rule-90 [0 0 0]) 0))) | |
(testing "rule 30" | |
;+-----------------------------------------------------------------+ | |
;| Neighborhood | 111 | 110 | 101 | 100 | 011 | 010 | 001 | 000 | | |
;+-----------------------------------------------------------------+ | |
;| New Center Cell | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | | |
;+-----------------------------------------------------------------+ | |
(is (= (rules/rule-30 [1 1 1]) 0)) | |
(is (= (rules/rule-30 [1 1 0]) 0)) | |
(is (= (rules/rule-30 [1 0 1]) 0)) | |
(is (= (rules/rule-30 [1 0 0]) 1)) | |
(is (= (rules/rule-30 [0 1 1]) 1)) | |
(is (= (rules/rule-30 [0 1 0]) 1)) | |
(is (= (rules/rule-30 [0 0 1]) 1)) | |
(is (= (rules/rule-30 [0 0 0]) 0)))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns cellular-animation.rules) | |
(def rule-90 | |
{[1 1 1] 0 | |
[1 1 0] 1 | |
[1 0 1] 0 | |
[1 0 0] 1 | |
[0 1 1] 1 | |
[0 1 0] 0 | |
[0 0 1] 1 | |
[0 0 0] 0}) | |
(def rule-30 | |
{[1 1 1] 0 | |
[1 1 0] 0 | |
[1 0 1] 0 | |
[1 0 0] 1 | |
[0 1 1] 1 | |
[0 1 0] 1 | |
[0 0 1] 1 | |
[0 0 0] 0}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns cellular-animation.evolution-test | |
(:require | |
[cljs.test :refer-macros [deftest testing is]] | |
[cellular-animation.evolution :as evolution] | |
[cellular-animation.rules :as rules])) | |
(deftest cellular-automaton-evolution | |
(testing "evolution from an initial state" | |
(is (= (evolution/evolve rules/rule-90 [[1]]) | |
[[0 1 0] | |
[1 0 1]])) | |
(is (= (evolution/evolve rules/rule-90 [[0 1 0] [1 0 1]]) | |
[[0 0 1 0 0] | |
[0 1 0 1 0] | |
[1 0 0 0 1]])))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns cellular-animation.evolution) | |
(defn- extract-neighborhoods [state] | |
(partition 3 1 (repeat 0) (cons 0 state))) | |
(defn- evolve-once [rule state] | |
(mapv rule (extract-neighborhoods state))) | |
(defn- pad-with-zeroes [state] | |
(vec (cons 0 (conj state 0)))) | |
(defn- next-state [rule state] | |
(->> state | |
pad-with-zeroes | |
(evolve-once rule))) | |
(defn evolve [rule states] | |
(->> (last states) | |
(next-state rule) | |
(conj (mapv pad-with-zeroes states)) | |
vec)) |
With that working, I played on the REPL to create a subscriber that reacted to changes on the cellular automaton states making the view render.
This is the resulting code of the view:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns cellular-animation.views | |
(:require | |
[re-frame.core :as re-frame])) | |
(def ^:private cell-representations | |
{0 "\u00A0" 1 "*"}) | |
(defn- render-state [state-index cell-index cell-state] | |
(with-meta | |
[:span.cell (cell-representations cell-state)] | |
{:key (str "cell-state-" state-index "-cell-" cell-index)})) | |
(defn- render-states [state-index state] | |
(with-meta | |
[:div | |
(map-indexed (partial render-state state-index) state)] | |
{:key (str "state-" state-index)})) | |
(defn- automaton-component [] | |
(let [automaton-states (re-frame/subscribe [:automaton-states])] | |
(fn [] | |
[:div | |
{:on-click #(re-frame/dispatch [:evolution-started-or-stopped])} | |
(map-indexed render-states @automaton-states)]))) | |
(defn main-panel [] | |
[automaton-component]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns cellular-animation.subs | |
(:require-macros | |
[reagent.ratom :refer [reaction]]) | |
(:require | |
[re-frame.core :as re-frame])) | |
(re-frame/reg-sub | |
:automaton-states | |
(fn [db] | |
(:automaton-states db))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns cellular-animation.db | |
(:require | |
[cellular-animation.rules :as rules])) | |
(def default-db | |
{:automaton-states [[1]] | |
:rule rules/rule-90 | |
:evolving false}) |
The use of effects keeps re-frame handlers pure. They allow us to avoid making side effects. We just have to describe as data the computation that will be made instead of doing it. re-frame takes care of that part.
These are the handlers tests:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns cellular-animation.handlers-test | |
(:require | |
[cljs.test :refer-macros [deftest testing is]] | |
[cellular-animation.handlers :as handlers] | |
[cellular-animation.rules :as rules])) | |
(deftest evolve-handler-test | |
(testing "when the automaton is evolving it calls itself after changing its state" | |
(let [initial-states [[1]] | |
expected-states [[0 1 0] [1 0 1]] | |
db {:automaton-states initial-states :rule rules/rule-90 :evolving true}] | |
(is (= (handlers/evolve-handler {:db db} []) | |
{:db (merge db {:automaton-states expected-states}) | |
:dispatch-later [{:ms 100 :dispatch [:evolve]}]})))) | |
(testing "when the automaton is not evolving it does not change its state" | |
(let [db {:automaton-states :not-used-initial-states :rule :not-used-rule :evolving false}] | |
(is (= (handlers/evolve-handler {:db db} []) {:db db}))))) | |
(deftest start-stop-evolution-test | |
(testing "when the automaton is evolving it stops the evolution" | |
(let [db {:evolving true}] | |
(is (= (handlers/start-stop-evolution {:db db} []) | |
{:db (assoc db :evolving false) | |
:dispatch [:evolve]})))) | |
(testing "when the automaton is not evolving it starts the evolution" | |
(let [db {:evolving false}] | |
(is (= (handlers/start-stop-evolution {:db db} []) | |
{:db (assoc db :evolving true) | |
:dispatch [:evolve]}))))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns cellular-animation.handlers | |
(:require | |
[cellular-animation.evolution :as evolution])) | |
(defn evolve-handler [{:keys [db]} _] | |
(if (:evolving db) | |
{:db (update db :automaton-states | |
(partial evolution/evolve (:rule db))) | |
:dispatch-later [{:ms 100 :dispatch [:evolve]}]} | |
{:db db})) | |
(defn start-stop-evolution [{:keys [db]} _] | |
{:db (update db :evolving not) | |
:dispatch [:evolve]}) |
To see the whole picture this is the code that registers the handlers:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns cellular-animation.register-handlers | |
(:require | |
[re-frame.core :as re-frame] | |
[cellular-animation.db :as db] | |
[cellular-animation.handlers :as handlers])) | |
(re-frame/reg-event-db | |
:initialize-db | |
(fn [_ _] | |
db/default-db)) | |
(re-frame/reg-event-fx | |
:evolution-started-or-stopped handlers/start-stop-evolution) | |
(re-frame/reg-event-fx | |
:evolve handlers/evolve-handler) |
They make testing handlers logic very easy (since handlers keep being pure functions) and avoid having to use test doubles.
Finally, everything is put together in the core namespace:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns cellular-animation.core | |
(:require | |
[reagent.core :as reagent] | |
[re-frame.core :as re-frame] | |
[devtools.core :as devtools] | |
[cellular-animation.register-handlers] | |
[cellular-animation.subs] | |
[cellular-animation.views :as views] | |
[cellular-animation.config :as config])) | |
(defn dev-setup [] | |
(when config/debug? | |
(enable-console-print!) | |
(println "dev mode") | |
(devtools/install!))) | |
(defn mount-root [] | |
(reagent/render [views/main-panel] | |
(.getElementById js/document "app"))) | |
(defn ^:export init [] | |
(re-frame/dispatch-sync [:initialize-db]) | |
(dev-setup) | |
(mount-root)) |
You can find the code on this GitHub repository.
In this post I've tried to, somehow, describe the process I followed to write this code, but the previous gists just reflect the final version of the code. If you want to follow how the code evolved, have a look to all the commits on Github.
I really love using ClojureScript and re-frame with Figwheel.