Index•Generated with Clerk from notebooks/rule_30.clj@8a1ea54

Rule 30 🕹

Let's explore cellular automata in a Clerk Notebook.

(ns rule-30
(:require [nextjournal.clerk :as clerk]))

We start by creating custom viewers for numbers, lists, and vectors.

These viewers are maps that contain a :pred (predicate) function that Clerk will use to decide which items should be viewed with the :render-fn that follows. Clerk always uses the first viewer whose predicate matches, so it's possible to override the built-in viewers with whatever we want.

In this case, we want 1s and 0s to show up as filled and empty boxes, lists to stack their contents vertically, and vectors to line up their contents horizontally. We achieve this with the html viewer, which allows us to emit arbitrary hiccup to represent a value.

(clerk/add-viewers!
[{:pred number?
:render-fn '(fn [n] [:div.inline-block {:style {:width 16 :height 16}
:class (if (pos? n) "bg-black" "bg-white border-solid border-2 border-black")}])}
{:pred (every-pred list? (partial every? (some-fn number? vector?)))
:render-fn '(fn [rows opts] (into [:div.flex.flex-col] (nextjournal.clerk.render/inspect-children opts) rows))}
{:pred (every-pred vector? (complement map-entry?) (partial every? number?))
:render-fn '(fn [row opts] (into [:div.flex.inline-flex] (nextjournal.clerk.render/inspect-children opts) row))}])
[{:pred #object[clojure.core$number_QMARK_ 0x112aa99c "
clojure.core$number_QMARK_@112aa99c"
]
:render-fn (fn [n] [:div.inline-block {:class (if (pos? n) "
bg-black"
"
bg-white border-solid border-2 border-black")
:style {:height
:width
}}])}
{:pred #object[clojure.core$every_pred$ep2__8612 0x514e8988 "
clojure.core$every_pred$ep2__8612@514e8988"
]
:render-fn (fn [rows opts] (into [:div.flex.flex-col] (nextjournal.clerk.render/inspect-children opts) rows))}
{:pred #object[clojure.core$every_pred$ep3__8627 0x36be711b "
clojure.core$every_pred$ep3__8627@36be711b"
]
:render-fn (fn [row opts] (into [:div.flex.inline-flex] (nextjournal.clerk.render/inspect-children opts) row))}
{:name nextjournal.clerk.viewer/header-viewer :transform-fn #object[clojure.core$comp$fn__5825 0x229ffb64 "
clojure.core$comp$fn__5825@229ffb64"
]
}
{:name nextjournal.clerk.viewer/char-viewer :pred #object[clojure.core$char_QMARK___5425 0x783b43df "
clojure.core$char_QMARK___5425@783b43df"
]
:render-fn (fn [c] [:span.cmt-string.inspected-value "
\"
c])}
{:closing-paren "
""
:name nextjournal.clerk.viewer/string-viewer :opening-paren "
""
:page-size
:pred #object[clojure.core$string_QMARK___5427 0xecf9049 "
clojure.core$string_QMARK___5427@ecf9049"
]
:render-fn nextjournal.clerk.render/render-quoted-string}
{:name nextjournal.clerk.viewer/number-viewer :pred #object[clojure.core$number_QMARK_ 0x112aa99c "
clojure.core$number_QMARK_@112aa99c"
]
:render-fn nextjournal.clerk.render/render-number :transform-fn #object[nextjournal.clerk.viewer$update_val$fn__13932 0x77f81eb "
nextjournal.clerk.viewer$update_val$fn__13932@77f81eb"
]
}
{:name nextjournal.clerk.viewer/number-hex-viewer :render-fn (fn [num] (nextjournal.clerk.render/render-number (str "
0x"
(.toString (js/Number. num)
))))}
{:name nextjournal.clerk.viewer/symbol-viewer :pred #object[clojure.core$symbol_QMARK_ 0x10027fc9 "
clojure.core$symbol_QMARK_@10027fc9"
]
:render-fn (fn [x] [:span.cmt-keyword.inspected-value (str x)])}
{:name nextjournal.clerk.viewer/keyword-viewer :pred #object[clojure.core$keyword_QMARK_ 0x52e10d07 "
clojure.core$keyword_QMARK_@52e10d07"
]
:render-fn (fn [x] [:span.cmt-atom.inspected-value (str x)])}
{:name nextjournal.clerk.viewer/nil-viewer :pred #object[clojure.core$nil_QMARK_ 0x246b2469 "
clojure.core$nil_QMARK_@246b2469"
]
:render-fn (fn [_] [:span.cmt-default.inspected-value "
nil"])}
{3 more elided} {4 more elided} {3 more elided} {2 more elided} {6 more elided} {6 more elided} {6 more elided} {5 more elided} {4 more elided} 25 more elided]

Now let's test each one to make sure they look the way we want:

0
1
[0 1 0]
(list 0 1 0)

Looks good! 😊

Rule 30 is implemented as a set of rules for translating one state to another, which can be represented as a map if transitions in Clojure. This definition maps any vector of three cells to a new value for the middle cell. Later, we'll scan over our state space, applying these rules to every position on the board. (Notice how the built-in map viewer works unchanged with our newly defined number and vector viewers.)

(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})
{
}

Our first generation is a row with 33 elements. The element at the center is a black square, all other squares are white.

(def first-generation
(let [n 33]
(assoc (vec (repeat n 0)) (/ (dec n) 2) 1)))

Finally, we can iterate over first-generation's state to evolve the state of the whole board over time. Try changing the value passed to take to render more states! Add a drop after the take to sample other points in time! Most of all, have fun.

(let [evolve #(mapv rule-30 (partition 3 1 (repeat 0) (cons 0 %)))]
(->> first-generation (iterate evolve) (take 17) (apply list)))