<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Ian Henry</title><description>Ian Henry's blog.</description><link>https://ianthehenry.com/posts/</link><atom:link href="https://ianthehenry.com/posts/feed.xml" rel="self" type="application/rss+xml"/><item><title>Periodic Spaces</title><description>&lt;p>One of my favorite SDF techniques is &lt;em>domain repetition&lt;/em>:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def eye-center [35 82 62])
(def eyes (ball 39 | move eye-center | mirror x :r 10))
(def eye-angle [0 0 0])
(defn pupils [target]
(def left-pupil
(ball 15 | shade black | move [39 0 0]
| align x target
| move eye-center
))
(def right-pupil
(ball 15 | shade black | move [39 0 0]
| align x target
| move [-1 1 1 * eye-center]
))
(union left-pupil right-pupil))
(defn get-target [seed i]
(hash3 seed i | remap- + [0 0 2] | normalize))
(defn eye-target [seed]
(def base (t + hash 30 seed * hash 20 seed))
(def frame (floor base + 100))
(mix
(get-target seed frame)
(get-target seed (frame + 1))
(ss (fract base) 0.49 0.51)))
(def anim (osc t 7 | ss 0.2 0.8))
(union :r 50
(ball [100 150 100] | move [0 7 3])
| shade sky
| union (expand eyes 20 | move [0 1 -43]) :r 10
| subtract :r 10 eyes
| union
(eyes | shade white | union-color (pupils (eye-target $i)))
| union (box :r 17 [(ss p.y -44 44 17 33) 44 17]
| morph (ball [29 44 17])
| rotate x 2.92
| move [0 12 100]
| shade orange)
| tile: $i [(anim * 400) (anim * 500) (anim * 500)] :limit [(anim * 10000) 8 (anim * 10000)]
| union (ground -150 | shade white)
| scale 0.25
| scale (ss anim 0 1 1 0.2)
| rotate y 0.45 x -0.33 z 0.08)
&lt;/code>&lt;/pre>&lt;p>It&amp;rsquo;s a great party trick that lets you render an infinite number of shapes in real-time, with soft shadows and ambient occlusion and all the other nice things that SDFs give you. It seemed like magic to me when I first saw it, and I suppose it still does &amp;ndash; albeit in a different way.&lt;/p>
&lt;p>The trick that makes this possible is that you aren&amp;rsquo;t evaluating &amp;ldquo;an infinite number of shapes.&amp;rdquo; As each ray marches through the scene, it only evaluates one shape at a time.&lt;/p>
&lt;p>Let&amp;rsquo;s look at a 2D slice of that scene:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def eye-center [35 82 62])
(def eyes (ball 39 | move eye-center | mirror x :r 10))
(def eye-angle [0 0 0])
(defn pupils [target]
(def left-pupil
(ball 15 | color black | move [39 0 0]
| align x target
| move eye-center
))
(def right-pupil
(ball 15 | color black | move [39 0 0]
| align x target
| move [-1 1 1 * eye-center]
))
(union left-pupil right-pupil))
(defn get-target [seed i]
(hash3 seed i | remap- + [0 0 2] | normalize))
(defn eye-target [seed]
(def base (t + hash 30 seed * hash 20 seed))
(def frame (floor base + 100))
(mix
(get-target seed frame)
(get-target seed (frame + 1))
(ss (fract base) 0.49 0.51)))
(def zed (osc t 5 10 25))
(union :r 50
(ball [100 150 100] | move [0 7 3])
| color sky
| union (expand eyes 20 | move [0 1 -43]) :r 10
| subtract :r 10 eyes
| union
(eyes | color white | union-color (pupils (eye-target $i)))
| union (box :r 17 [(ss p.y -44 44 17 33) 44 17]
| morph (ball [29 44 17])
| rotate x 2.92
| move [0 12 100]
| color orange)
| tile: $i [300 300 300]
| scale 0.25
| slice z zed)
&lt;/code>&lt;/pre>&lt;p>Actually, that&amp;rsquo;s freaking me out. Let&amp;rsquo;s look at a simpler 2D scene, and you can trust me that this generalizes to more complex shapes:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(star 20 10 :r 1
| color (hsv (hash $i) 0.5 1)
| tile: $i [80 80])
&lt;/code>&lt;/pre>&lt;p>Pretend that that&amp;rsquo;s a 2D slice of a 3D scene. Like, imagine that we&amp;rsquo;re actually trying to render this:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(star 20 10 :r 1
| shade (hsv (hash $i) 0.5 1)
| tile: $i [80 80]
| extrude z 20)
&lt;/code>&lt;/pre>&lt;p>In order to render a 3D scene, we shoot rays out from the camera at different angles until they hit something. It&amp;rsquo;s just much easier to visualize this process in 2D:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def scene
(star 20 10 :r 1
| color (hsv (hash $i) 0.5 1)
| tile: $i [80 80]))
(def line-start [-200 28])
(def line-end [68 64])
(def progress (t / 3 | fract | ss 0 0.5))
(def indicator (union
(line line-start (mix line-start line-end progress) 2)
(circle (mix -5 5 (ss progress 0.95 1)) | move line-end)))
(union
scene
(indicator | expand 1 | color black)
(indicator | color white))
&lt;/code>&lt;/pre>&lt;p>But when we&amp;rsquo;re rendering SDFs, we don&amp;rsquo;t cast rays continuously like this &amp;ndash; they don&amp;rsquo;t smoothly advance until they hit a shape. Since we know the distance field at every point, we use that information to decide how far to advance. Actual ray marching looks more like this:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def scene
(star 20 10 :r 1
| color (hsv (hash $i) 0.5 1)
| tile: $i [80 80] :limit 5))
(def direction [1 0.13 | normalize])
(def line-start [-200 28])
(gl/defn :vec3 march [:int iterations]
(var at line-start)
(var dist 0)
(for (var i 0:s) (&amp;lt;= i iterations) (++ i)
(+= at (dist * direction))
(set dist ,(gl/with [q at]
(shape/distance scene))))
(return [dist at]))
(def iterations 15)
(def line-end (. (march (int iterations)) yz))
(def frame-base (fract (t / 15) * iterations))
(def frame (frame-base | floor | int))
(def inframe (frame-base | fract | ss 0.5 0.6))
(gl/def highlight-circle (march frame))
(def i-dist highlight-circle.x)
(def i-progress highlight-circle.yz)
(defn ray-point [progress]
(line-end - line-start * progress + line-start))
(def next-point (i-dist * direction + i-progress))
(def indicator (union
(line line-start (mix i-progress next-point inframe) 2)
(circle i-dist | shell 2 | move i-progress)
))
(union
scene
(indicator | expand 1 | color black)
(indicator | color white))
&lt;/code>&lt;/pre>&lt;p>&amp;hellip;except, you know, faster.&lt;/p>
&lt;p>The radius of that circle is the value of the distance field at each point of the ray&amp;rsquo;s journey. The distance value means &amp;ldquo;what&amp;rsquo;s the distance to the nearest shape,&amp;rdquo; and during ray marching you can interpret that as &amp;ldquo;how far can I move in any direction before I hit something.&amp;rdquo; When you&amp;rsquo;re ray marching in 3D, it gives you the radius of the largest empty sphere of space around the current position of the ray.&lt;/p>
&lt;p>The trick that makes domain repetition possible is that, in order to compute the distance field of an infinite number of shapes, you actually only look at one shape at a time. It&amp;rsquo;s not &lt;em>actually&lt;/em> &amp;ldquo;the distance to the nearest shape.&amp;rdquo; It&amp;rsquo;s &amp;ldquo;the distance to the shape in the same &amp;lsquo;cell&amp;rsquo; as me.&amp;rdquo;&lt;/p>
&lt;p>That is, when we repeat this, we&amp;rsquo;re really breaking it up into cells, and only checking &amp;ldquo;the current cell:&amp;rdquo;&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def scene
(star 20 10 :r 1
| color (hsv (hash $i) 0.5 1)
| tile: $i [80 80] :limit 5))
(def direction [1 0.13 | normalize])
(def line-start [-200 28])
(gl/defn :vec3 march [:int iterations]
(var at line-start)
(var dist 0)
(for (var i 0:s) (&amp;lt;= i iterations) (++ i)
(+= at (dist * direction))
(set dist ,(gl/with [q at]
(shape/distance scene))))
(return [dist at]))
(def iterations 15)
(def line-end (. (march (int iterations)) yz))
(def frame-base (fract (t / 15) * iterations))
(def frame (frame-base | floor | int))
(def inframe (frame-base | fract | ss 0.5 0.6))
(gl/def highlight-circle (march frame))
(def dist highlight-circle.x)
(def ray-at highlight-circle.yz)
(defn ray-point [progress]
(line-end - line-start * progress + line-start))
(def next-point (dist * direction + ray-at))
(def indicator (union
(line line-start (mix ray-at next-point inframe) 2)
(circle dist | shell 2 | move ray-at)
))
(def ray-cell (ray-at / 80 | round))
(def scene
(star 20 10 :r 1
| color (hsv (hash $i) 0.5 (gl/if (= ray-cell $i) 1 0.25))
| tile: $i [80 80] :limit 5))
(union
(rect 40 | shell 1 | color white | tile: $i [80 80] :limit 5)
scene
(indicator | expand 1 | color black)
(indicator | color white))
&lt;/code>&lt;/pre>&lt;p>And this works. For fairly regular, symmetric shapes, this works.&lt;/p>
&lt;p>But part of the fun of domain repetition is that you don&amp;rsquo;t have to use regular shapes. You can do this with arbitrarily asymmetric shapes, or shapes that vary depending on the cell they&amp;rsquo;re in.&lt;/p>
&lt;p>In fact I&amp;rsquo;ve been doing that in all of these examples: the stars are colored differently based on their cell coordinates. But we can vary their shape as well:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn make-scene [f]
(star r (r / 2) :r 1
| gl/let [r (10 * hash $i 10 + 10)] _
| rotate (hash $i 130 | remap-)
| move (hash2 $i 140 | remap- * 20)
| f $i
| tile: $i [80 80] :limit 5))
(def scene (make-scene (fn [$ $i] $)))
(def direction [1 0.13 | normalize])
(def line-start [-200 28])
(gl/defn :vec3 march [:int iterations]
(var at line-start)
(var dist 0)
(for (var i 0:s) (&amp;lt;= i iterations) (++ i)
(+= at (dist * direction))
(set dist ,(gl/with [q at]
(shape/distance scene))))
(return [dist at]))
(def iterations 15)
(def line-end (. (march (int iterations)) yz))
(def frame-base (fract (t / 15) * iterations))
(def frame (frame-base | floor | int))
(def inframe (frame-base | fract | ss 0.5 0.6))
(gl/def highlight-circle (march frame))
(def dist highlight-circle.x)
(def ray-at highlight-circle.yz)
(defn ray-point [progress]
(line-end - line-start * progress + line-start))
(def next-point (dist * direction + ray-at))
(def indicator (union
(line line-start (mix ray-at next-point inframe) 2)
(circle (abs dist) | shell 2 | move ray-at)
))
(def ray-cell (ray-at / 80 | round))
(def scene (make-scene (fn [$ $i]
($ | color (hsv (hash $i) 0.5 (gl/if (= ray-cell $i) 1 0.25))))))
(union
(rect 40 | shell 1 | color white | tile: $i [80 80] :limit 5)
scene
(indicator | expand 1 | color black)
(indicator | color white))
&lt;/code>&lt;/pre>&lt;p>Hey look! We have a problem. If you watch that march to its completion, you&amp;rsquo;ll see that the ray actually overshoots, and winds up in the middle of a star. Thanks to the power of &lt;em>signed&lt;/em> distance functions, it&amp;rsquo;s able to realize that it&amp;rsquo;s inside a shape (the distance field is negative) and the ray marcher backs out until it finds the edge.&lt;/p>
&lt;p>This demonstrates a neat fact about signed distance functions, but the real point is that our magical approximation of infinite distance isn&amp;rsquo;t so magical after all. We won&amp;rsquo;t always be able to back out like this: with a little worse luck, a bad distance field could cause us to overshoot completely.&lt;/p>
&lt;p>We&amp;rsquo;ll get obvious visual artifacts when we use this technique to render a 3D scene:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(star 20 10 :r 1 | extrude z 3
| rotate (hash3 $i 10 | normalize) t
| move (hash3 $i 20 | remap- * 10)
| shade (hsv (hash $i) 0.6 1)
| tile: $i [50 50 50] :limit 5)
&lt;/code>&lt;/pre>&lt;p>As only some pixels overshoot their destinations.&lt;/p>
&lt;p>And we can mitigate this by evaluating not &lt;em>just&lt;/em> the current cell, but the current cell and its immediate neighbors. Your distance function gets more expensive &amp;ndash; you&amp;rsquo;re evaluating your shape N times for each march now &amp;ndash; but it&amp;rsquo;s a small price to pay for infinity.&lt;/p>
&lt;p>Here&amp;rsquo;s the exact same scene, but sampling the eight nearest shapes instead of the nearest one:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(star 20 10 :r 1 | extrude z 3
| rotate (hash3 $i 10 | normalize) t
| move (hash3 $i 20 | remap- * 10)
| shade (hsv (hash $i) 0.6 1)
| tile: $i [50 50 50] :limit 5 :oversample true)
&lt;/code>&lt;/pre>&lt;p>The artifacts are gone, but my laptop is hot now.&lt;/p>
&lt;p>Okay. So this is classic domain repetition: it&amp;rsquo;s a &lt;em>discrete&lt;/em> operation. There is a concept of a discrete &amp;ldquo;cell&amp;rdquo; or &amp;ldquo;tile,&amp;rdquo; with integer coordinates, and evaluation is based around multiple instances of these discrete cells.&lt;/p>
&lt;p>Great.&lt;/p>
&lt;p>Background information: explained.&lt;/p>
&lt;p>Now we can start the blog post.&lt;/p>
&lt;h1 id="periodic-spaces">Periodic Spaces&lt;/h1>
&lt;p>Here&amp;rsquo;s a star:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(star 50 25 :r 3 | rotate (t / 2) | color sky)
&lt;/code>&lt;/pre>&lt;p>There is only one star there. It is a normal star.&lt;/p>
&lt;p>Here is a plot of the x coordinate of our input (on the x axis) versus the x coordinate that we render at (on the y axis).&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(union
(star 50 25 :r 3 | rotate (t / 2) | color sky)
(circle 1 | move [q.x q.x] | color yellow))
&lt;/code>&lt;/pre>&lt;p>It is the identity function, because we aren&amp;rsquo;t doing anything weird yet.&lt;/p>
&lt;p>Let&amp;rsquo;s do something weird.&lt;/p>
&lt;p>For each pixel we&amp;rsquo;re rendering, instead of using the color of the star at that point, we can use the color of the star at a &lt;em>different&lt;/em> point. A trivial example is to just scale the x coordinate:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn with-x [expr]
(gl/with [q [expr q.y]]
(union
(star 50 25 :r 3 | rotate (t / 2) | color sky)
(circle 1 | move [q.x q.x] | color yellow))))
(with-x (q.x / 2))
&lt;/code>&lt;/pre>&lt;p>The effect that we see is a stretching effect: it looks like we scaled the star. But we didn&amp;rsquo;t &lt;em>really&lt;/em>: we scaled the &lt;em>image&lt;/em> of the star that we rendered. This is like if we put a star on a scanner and slid it across the glass as it scanned. We would get an image that &lt;em>appeared&lt;/em> stretched &amp;ndash; just like this.&lt;/p>
&lt;p>Many SDF combinators work this way. If you want to move the star ten pixels to the right, you actually evaluate the star at a point ten pixels to the left.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn with-x [expr]
(gl/with [q [expr q.y]]
(union
(star 50 25 :r 3 | rotate (t / 2) | color sky)
(circle 1 | move [q.x q.x] | color yellow))))
(with-x (q.x + oss t 5 -50 50))
&lt;/code>&lt;/pre>&lt;p>But notice: we can do anything we want to the input. It&amp;rsquo;s just an expression, and we can write whatever expression we want.&lt;/p>
&lt;p>We could evaluate at a random x coordinate:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn with-x [expr]
(gl/with [q [expr q.y]]
(union
(star 50 25 :r 3 | rotate (t / 2) | color sky)
(circle 1 | move [q.x q.x] | color yellow))))
(with-x (q.x | hash | remap- * 100))
&lt;/code>&lt;/pre>&lt;p>We could evaluate with a smoothly-varying, semi-random offset:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn with-x [expr]
(gl/with [q [expr q.y]]
(union
(star 50 25 :r 3 | rotate (t / 2) | color sky)
(circle 1 | move [q.x q.x] | color yellow))))
(with-x (perlin [q.x 0] 20 * 20 + q.x))
&lt;/code>&lt;/pre>&lt;p>We can even go nuts and make x a function of x and y:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn with-x [expr]
(gl/with [q [expr q.y]]
(union
(star 50 25 :r 3 | rotate (t / 2) | color sky)
(circle 1 | move [q.x q.x] | color yellow))))
(with-x (perlin q [30 100] * 100))
&lt;/code>&lt;/pre>&lt;p>Or we could get back to the point of this blog post, and evaluate x with a periodic function. What happens if we take &lt;code>x % 100&lt;/code>?&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn with-x [expr]
(gl/with [q [expr q.y]]
(union
(star 50 25 :r 3 | rotate (t / 2) | color sky)
(circle 1 | move [q.x q.x] | color yellow))))
(with-x (mod q.x 100))
&lt;/code>&lt;/pre>&lt;p>Er, right. That doesn&amp;rsquo;t look very good, because the star is centered at zero. Let&amp;rsquo;s try a variant of modulo that centers its output around zero:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn with-x [expr]
(gl/with [q [expr q.y]]
(union
(star 50 25 :r 3 | rotate (t / 2) | color sky)
(circle 1 | move [q.x q.x] | color yellow))))
(defn mod- [a b] (a + (b / 2) | mod b - (b / 2)))
(with-x (mod- q.x 100))
&lt;/code>&lt;/pre>&lt;p>Aha! We have re-created the &lt;code>tile&lt;/code> function that we were using to repeat space at the beginning of this blog post. And the plot of our x coordinate now looks like a sawtooth wave. It&amp;rsquo;s discontinuous &amp;ndash; as soon as we reach the end of one period, we jump right back to the next period.&lt;/p>
&lt;p>This sharp discontinuity doesn&amp;rsquo;t matter in 2D. But let&amp;rsquo;s take a look at this exact same scene extruded into 3D:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn with-x [expr]
(gl/with [q [expr q.y]]
(star 50 25 :r 3 | rotate (t / 2) | shade sky)))
(defn mod- [a b] (a + (b / 2) | mod b - (b / 2)))
(with-x (mod- q.x 100) | extrude z 3)
&lt;/code>&lt;/pre>&lt;p>There are visual artifacts around the tips of the stars: sometimes the ray overshoots, because this repetition does not produce a correct distance field. (You can also click the magnet icon in the top right of the image for an alternate visualization that will make these artifacts stand out more clearly.)&lt;/p>
&lt;p>We can see that the distance field is not very good by looking at the gradient of the distance field for this scene:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn with-x [expr]
(gl/with [q [expr q.y]]
(union
(star 50 25 :r 3 | rotate (t / 2)))))
(defn mod- [a b] (a + (b / 2) | mod b - (b / 2)))
(with-x (mod- q.x 100))
&lt;/code>&lt;/pre>&lt;p>Notice how, at the sharp lines dividing each period of the wave, the contour lines don&amp;rsquo;t match up (it&amp;rsquo;s easier to see if you click to pause the rotation). If we use Bauble&amp;rsquo;s built-in multisampled repetition operator, we can see what the distance field &lt;em>should&lt;/em> look like:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(star 50 25 :r 3 | rotate (t / 2) | tile [100 0] :oversample true)
&lt;/code>&lt;/pre>&lt;p>But our sawtooth-wave modulo-repetition doesn&amp;rsquo;t look like that. We don&amp;rsquo;t see the complex distance field between two stars, because we never evaluate two stars. We just evaluate our one lonely star at different coordinates.&lt;/p>
&lt;p>So this sawtooth wave is not great &amp;ndash; although this is an interesting way to interpret the (naive) tiling operator.&lt;/p>
&lt;p>Let&amp;rsquo;s try another periodic function. What if we use a triangle wave?&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn with-x [expr]
(gl/with [q [expr q.y]]
(union
(star 50 25 :r 3 | rotate (t / 2) | color sky)
(circle 1 | move [q.x q.x] | color yellow))))
(gl/defn :float triangle-wave [:float x :float period]
(var section (x / period))
(var dir (round section | mod 2 | remap- | -))
(return (fract (section + 0.5) - 0.5 * period * dir)))
(with-x (triangle-wave q.x 100))
&lt;/code>&lt;/pre>&lt;p>An intuitive explanation of this is: when you reach the end of the scanner, instead of starting over, reverse and scan backwards. So we &amp;ldquo;scan&amp;rdquo; left and right over the star, and the effect is that every other star is mirrored.&lt;/p>
&lt;p>And, surprisingly to me, this actually &lt;em>does&lt;/em> produce a correct distance field:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn with-x [expr]
(gl/with [q [expr q.y]]
(star 50 25 :r 3 | rotate (t / 2))))
(gl/defn :float triangle-wave [:float x :float period]
(var section (x / period))
(var dir (round section | mod 2 | remap- | -))
(return (fract (section + 0.5) - 0.5 * period * dir)))
(with-x (triangle-wave q.x 100))
&lt;/code>&lt;/pre>&lt;p>Notice how, whenever the period changes, the contour lines perfectly match up. This means that we really can ray march this perfectly in 3D, with no visual artifacts and no extra evaluation:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn with-x [expr]
(gl/with [q [expr q.y]]
(star 50 25 :r 3 | rotate (t / 2) | shade sky)))
(gl/defn :float triangle-wave [:float x :float period]
(var section (x / period))
(var dir (round section | mod 2 | remap- | -))
(return (fract (section + 0.5) - 0.5 * period * dir)))
(with-x (triangle-wave q.x 100) | extrude z 3)
&lt;/code>&lt;/pre>&lt;p>Of course it&amp;rsquo;s not the &lt;em>same&lt;/em> scene that we were rendering before &amp;ndash; half the stars are flipped &amp;ndash; but still. If you&amp;rsquo;re tiling in all three dimensions, this is an 8x speedup over multisampled evaluation.&lt;/p>
&lt;p>Now, &lt;a href="https://iquilezles.org/articles/sdfrepetition/">this effect is well-known&lt;/a>, and you can apply this to classic instanced repetition as well. If you want to repeat an asymmetric shape, you can make it the whole distance field symmetric by mirroring every other instance. But it didn&amp;rsquo;t really click for me until I saw it as a periodic function of space like this.&lt;/p>
&lt;p>What about a sine wave?&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn with-x [expr]
(gl/with [q [expr q.y]]
(union
(star 50 25 :r 3 | rotate (t / 2) | color sky)
(circle 1 | move [q.x q.x] | color yellow))))
(gl/defn :float sine-wave [:float x :float period]
(return (sin (x * pi / period) * 0.5 * period)))
(with-x (sine-wave q.x 100))
&lt;/code>&lt;/pre>&lt;p>This one is pretty weird. It&amp;rsquo;s not a correct distance field: the contour lines are not equal distances apart, because space is stretched nonlinearly near the edges of each tile.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn with-x [expr]
(gl/with [q [expr q.y]]
(star 50 25 :r 3 | rotate (t / 2))))
(gl/defn :float sine-wave [:float x :float period]
(return (sin (x * pi / period) * 0.5 * period)))
(with-x (sine-wave q.x 100))
&lt;/code>&lt;/pre>&lt;p>Still, we can ray march it with &lt;em>minimal&lt;/em> artifacts:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn with-x [expr]
(gl/with [q [expr q.y]]
(star 50 25 :r 3 | rotate (t / 2) | shade sky)))
(gl/defn :float sine-wave [:float x :float period]
(return (sin (x * pi / period) * 0.5 * period)))
(with-x (sine-wave q.x 100) | extrude z 3)
&lt;/code>&lt;/pre>&lt;p>It&amp;rsquo;s nice to be able to achieve a &amp;ldquo;smooth tiling&amp;rdquo; effect like this. As we vary the period, the shapes smoothly join together instead of just getting chopped off:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn with-x [expr]
(gl/with [q [expr q.y]]
(star 50 25 :r 3 | rotate (t / 2) | shade sky)))
(gl/defn :float sine-wave [:float x :float period]
(return (sin (x * pi / period) * 0.5 * period)))
(with-x (sine-wave q.x (osc t 5 100 50)) | extrude z 3)
&lt;/code>&lt;/pre>&lt;p>It&amp;rsquo;s a neat effect in full 3D, too:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn with-point [expr]
(gl/with [p expr]
(box 25 | rotate x (t / 2) z (t / 3) y (t / 5) | shade sky)))
(gl/defn :float sine-wave [:float x :float period]
(return (sin (x * pi / period) * 0.5 * period)))
(defn limit [f x period instances]
(gl/if (&amp;gt; (abs x) (period * instances))
(x - (period * instances * sign x))
(f x period)
))
(with-point
[(limit sine-wave p.x (osc t 3 50 75) 2)
(limit sine-wave p.y (osc t 3 50 75) 2)
(limit sine-wave p.z (osc t 3 50 75) 2)])
&lt;/code>&lt;/pre>&lt;p>You can also use this trick with radial symmetry. Bauble&amp;rsquo;s built-in &lt;code>radial&lt;/code> symmetry operation uses the same discrete approach as its &lt;code>tile&lt;/code> operation:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(octahedron 100 :r 5
| rotate x (t / 2) z (t / 3)
| radial y 12 80
| shade sky)
&lt;/code>&lt;/pre>&lt;p>Artifact city, and sharp lines at the edge of each period.&lt;/p>
&lt;p>But if we write our own radial symmetry, we can swap that sawtooth for a sine wave:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(gl/defn :float sine-wave [:float x :float period]
(return (sin (x * pi / period) * 0.5 * period)))
(gl/def angle (sine-wave (atan2+ p.xz) (tau / 12)))
(gl/def dist (length p.xz))
(octahedron 100 :r 5
| rotate x (t / 2) z (t / 3)
| move x 80
| gl/with [p [(cos angle * dist) p.y (sin angle * dist)]] _
| shade sky)
&lt;/code>&lt;/pre>&lt;p>Kind of interesting.&lt;/p>
&lt;p>This is a useful spatial distortion, and it&amp;rsquo;s fun to explore periodic spaces like this. I think there&amp;rsquo;s some value especially in a cheap approximation of smoothly merged repeated surfaces: that&amp;rsquo;s another effect that you &lt;em>can&lt;/em> achieve with multisampled repetition &amp;ndash; you don&amp;rsquo;t have to take the &lt;code>min&lt;/code> of all adjacent cells; you can apply a smooth union operation &amp;ndash; but the speedup you get by just throwing a sine wave at it is significant.&lt;/p>
&lt;p>But this technique is less powerful than classic instanced domain repetition. For one thing, we&amp;rsquo;re always repeating the &lt;em>same&lt;/em> shape. And while we could invent our own notion of &amp;ldquo;cell coordinates&amp;rdquo; based on which period we&amp;rsquo;re in, and still vary the shape as we scanned it with a triangle wave, by doing so we would lose the symmetry that made the triangle wave appealing in the first place.&lt;/p>
&lt;p>There&amp;rsquo;s another limitation that hasn&amp;rsquo;t mattered in any of our examples yet: using classic instanced repetition, you have the choice to sample more than just the immediately adjacent cells. This allows you to repeat shapes that aren&amp;rsquo;t bounded by a single cell.&lt;/p>
&lt;p>For example, here&amp;rsquo;s a triangle radially tiled around the origin:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(triangle [25 100]
| color (hsv (hash $i) 0.5 1)
| rotate (-pi/2 + t)
| radial: $i 10 50)
&lt;/code>&lt;/pre>&lt;p>Because we&amp;rsquo;re only evaluating one &amp;ldquo;slice&amp;rdquo; of space, the triangle is clipped where it crosses the boundary between two cells. By evaluating the nearest adjacent slice as well, we can improve this:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(triangle [25 100]
| color (hsv (hash $i) 0.5 1)
| rotate (-pi/2 + t)
| radial: $i 10 50 :oversample true
:sample-from 0
:sample-to 1)
&lt;/code>&lt;/pre>&lt;p>But it&amp;rsquo;s still clipped. Let&amp;rsquo;s evaluate both adjacent slices:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(triangle [25 100]
| color (hsv (hash $i) 0.5 1)
| rotate (-pi/2 + t)
| radial: $i 10 50 :oversample true
:sample-from -1
:sample-to 1)
&lt;/code>&lt;/pre>&lt;p>Better! But still not perfect. Let&amp;rsquo;s evaluate both adjacent slices, and the adjacent slices next to them:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(triangle [25 100]
| color (hsv (hash $i) 0.5 1)
| rotate (-pi/2 + t)
| radial: $i 10 50 :oversample true
:sample-from -2
:sample-to 2)
&lt;/code>&lt;/pre>&lt;p>That&amp;rsquo;s not something you can do with a pure periodic function of space: there&amp;rsquo;s no way to have a slice that contains five different triangles.&lt;/p>
&lt;p>But even so&amp;hellip; what other periodic functions should we try?&lt;/p></description><pubDate>Sun, 30 Nov 2025 00:00:00 +0000</pubDate><link>https://ianthehenry.com/posts/periodic-spaces/</link><guid isPermaLink="true">https://ianthehenry.com/posts/periodic-spaces/</guid></item><item><title>Generalized Worley Noise</title><description>&lt;p>&lt;em>Worley noise&lt;/em> is a type of noise used for procedural texturing in computer graphics. In its most basic form, it looks like this:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(color r2 (worley q 50 :oversample true))
&lt;/code>&lt;/pre>&lt;p>That&amp;rsquo;s ugly and boring, but it&amp;rsquo;s a quick way to see what the effect looks like. If we use Worley noise to distort a 3D shape, we can get something like a hammered or cratered texture:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def s (osc t 5 | ss 0.2 0.8 * 30 + 10))
(ball 100
| expound (worley p s) (sqrt s)
| slow 0.8
| shade sky
| rotate y (t / 10))
&lt;/code>&lt;/pre>&lt;p>Like many procedural textures, it looks a lot better if you repeat the effect a few times with different frequencies:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def s (osc t 5 | ss 0.2 0.8 * 30 + 10))
(ball 100
| expound (fbm 3 worley p s) (sqrt s)
| slow 0.8
| shade sky
| rotate y (t / 10))
&lt;/code>&lt;/pre>&lt;p>There are some visual artifacts in these renderings, because they&amp;rsquo;re using a fast approximation of Worley noise that gives the wrong answer for some values.&lt;/p>
&lt;p>That&amp;rsquo;s not very satisfying, but in order to explain where these artifacts are coming from, we first have to talk about how Worley noise works.&lt;/p>
&lt;p>It&amp;rsquo;s pretty simple: you start with a grid of points.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(circle 0
| color (hsv (hash $i) 0.5 1)
| tile: $i [30 30]
| expand 3)
&lt;/code>&lt;/pre>&lt;p>Then you move each point by some random offset:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def animate (osc t 5 | ss 0.1 0.7))
(circle 0
| color (hsv (hash $i) 0.5 1)
| move (hash2 $i | remap- * animate * 10)
| tile: $i [30 30] :oversample true
| expand 3)
&lt;/code>&lt;/pre>&lt;p>When you&amp;rsquo;re writing a shader, you can&amp;rsquo;t actually generate random numbers, so we&amp;rsquo;re using a hash function to produce random-&lt;em>looking&lt;/em> offsets based on the logical position of each point (that is, &lt;code>$i = [0 0]&lt;/code> for the center point, &lt;code>$i = [1 0]&lt;/code> for the point to the right of that, etc).&lt;/p>
&lt;p>Finally, once you have the points at random-looking positions, you compute the distance to the nearest point for every individual pixel in your input &amp;ndash; and that&amp;rsquo;s Worley noise.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def animate (osc t 5 | ss 0.1 0.7))
(def points
(circle 0
| color (hsv (hash $i) 0.5 1)
| move (hash2 $i | remap- * animate * 10)
| tile: $i [30 30] :oversample true
))
(set background-color
(vec3 (shape/distance points / 30)))
(expand points 3)
&lt;/code>&lt;/pre>&lt;p>How do you compute the distance to the nearest point for any pixel you ask about? It&amp;rsquo;s actually pretty simple: you know that you started with a perfectly even square grid. For any pixel, you can compute the &amp;ldquo;grid cell&amp;rdquo; that that pixel falls into (&lt;code>[0 0]&lt;/code>, &lt;code>[0 1]&lt;/code>, etc). It&amp;rsquo;s just the pixel divided by the grid size, rounded to the nearest integer.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def animate (osc t 5 | ss 0.1 0.7))
(union
(color r2 (hsv (hash $i) 0.5 1))
(circle 3 | color (hsv (hash $i) 0.5 0.1))
| move (hash2 $i | remap- * animate * 10)
| tile: $i [30 30] :oversample true :sample-from -1
)
&lt;/code>&lt;/pre>&lt;p>And you know that the nearest point is either in this grid, or it&amp;rsquo;s in one of the immediately adjacent grids, because we only offset our points by &lt;em>at most&lt;/em> half the grid size, so each randomly distributed point is still inside its original grid cell. Which means there&amp;rsquo;s no point inside any other cell that could be nearer than any point in one of the adjacent cells.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def animate (osc t 5 | ss 0.1 0.7))
(def query-point (rotate [(osc t 5 50 100) 0] (t / 2)))
(def query-cell (query-point / 30 - 1 | round))
(union
(color r2 (hsv (hash $i) 0.5
(gl/if (&amp;lt;= (max ($i - query-cell | abs)) 1) 1 0.25)))
(circle 3 | color (hsv (hash $i) 0.5 0.1))
| move (hash2 $i | remap- * animate * 10)
| tile: $i [30 30] :oversample true :sample-from -1
| union
(union (circle 4 | color black) (circle 3 | color white)
| move query-point))
&lt;/code>&lt;/pre>&lt;p>So that leaves you nine points to check, for every single pixel in your shader. Here&amp;rsquo;s the optimization that&amp;rsquo;s causing visual artifacts: instead of checking all eight adjacent points, only check the three nearest points. The nearest point to your sample position is &lt;em>probably&lt;/em> in one of those cells, but it doesn&amp;rsquo;t &lt;em>have&lt;/em> to be, so whenever you get unlucky you wind up with some visual artifacts.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def animate (osc t 5 | ss 0.1 0.7))
(def query-point (rotate [(osc t 5 50 100) 0] (t / 2)))
(def query-cell (query-point / 30 - 1 | round))
(def query-bias (query-point / 30 | fract | round * 2 - 1 -))
(defn all [bvec] (and bvec.x bvec.y))
(defn or-bvec [a b] [(or a.x b.x) (or a.y b.y)])
(union
(color r2 (hsv (hash $i) 0.5
(gl/let [offset ($i - query-cell)]
(gl/if (and (&amp;lt;= (max (abs offset)) 1)
(all
(or-bvec (equal offset query-bias)
(equal offset [0 0]))))
1 0.25))))
(circle 3 | color (hsv (hash $i) 0.5 0.1))
| move (hash2 $i | remap- * animate * 10)
| tile: $i [30 30] :oversample true :sample-from -1
| union
(union (circle 4 | color black) (circle 3 | color white)
| move query-point))
# ahh that took so long
&lt;/code>&lt;/pre>&lt;p>9 points to 4 points is a nice improvement in 2D, but in 3D this optimization takes you from 27 points to 8 points, which can be the difference between realtime and offline rendering.&lt;/p>
&lt;p>But notice: this is getting a little bit complicated. And the original code snippet I showed you wasn&amp;rsquo;t very complicated at all:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def animate (osc t 5 | ss 0.1 0.7))
(def points
(circle 0
| color (hsv (hash $i) 0.5 1)
| move (hash2 $i | remap- * animate * 10)
| tile: $i [30 30] :oversample true
))
(set background-color
(vec3 (shape/distance points / 30)))
(expand points 3)
&lt;/code>&lt;/pre>&lt;p>Nowhere does that code compute cell coordinates or check for the nearest point. I just constructed this &lt;em>thing&lt;/em>, said &lt;code>shape/distance&lt;/code>, and somehow that just&amp;hellip; gave me the distance to the nearest point.&lt;/p>
&lt;p>I was able to do that because &lt;a href="https://bauble.studio/">Bauble&lt;/a> is a playground for making 3D graphics with &lt;a href="https://en.wikipedia.org/wiki/Signed_distance_function">signed distance functions&lt;/a>. Bauble&amp;rsquo;s &lt;em>whole deal&lt;/em> is computing distances to things! And Worley noise is just the signed distance function of a bunch of randomly distributed points. I&amp;rsquo;m used to thinking of signed distance functions as defining implicit surfaces of 3D shapes, but Worley noise uses the distance as a scalar in its own right.&lt;/p>
&lt;p>So.&lt;/p>
&lt;p>This is interesting.&lt;/p>
&lt;p>What if&amp;hellip; we took &lt;em>other&lt;/em> signed distance functions, and used them as procedural noise distortions?&lt;/p>
&lt;p>We&amp;rsquo;ll start simple. Instead of points, what if we randomly distribute a bunch of squares?&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def animate (osc t 5 | ss 0.1 0.7))
(def size 60)
(def points
(rect size
| color (hsv (hash $i) 0.5 1)
| move (hash2 $i | remap- * animate * 10)
| tile: $i (0.5 * size | vec2) :oversample true
))
(set background-color
(vec3 (shape/distance points + size / (0.5 * size))))
(expand points (- 3 size))
&lt;/code>&lt;/pre>&lt;p>It&amp;rsquo;s not obvious that that will be interesting. Let&amp;rsquo;s look at it in action:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn squarley [input &amp;amp;opt period]
(default period 1)
(gl/with [q (input / period)]
(rect 1
| color (hsv (hash $i) 0.5 1)
| move (hash2 $i | remap- * 0.33)
| tile: $i [1 1] :oversample true
| shape/distance)))
(ball 100 | expound (squarley p.xy 20) 20
| rotate y (t / 10))
&lt;/code>&lt;/pre>&lt;p>Since we only defined this noise function in 2D, we need a two-dimensional input. That&amp;rsquo;s a pretty boring 2D input. This is a little more interesting:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn squarley [input &amp;amp;opt period]
(default period 1)
(gl/with [q (input / period)]
(rect 1
| color (hsv (hash $i) 0.5 1)
| move (hash2 $i | remap- * 0.33)
| tile: $i [1 1] :oversample true
| shape/distance)))
(ball 100
| expound (squarley
[(atan2 p.xy | abs) (sqrt (150 - p.z | abs))]
[.5 1]) 20
| rotate y (t / 10))
&lt;/code>&lt;/pre>&lt;p>We can apply multiple octaves of this, to get&amp;hellip; &lt;em>something&lt;/em>.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn squarley [input &amp;amp;opt period]
(default period 1)
(gl/with [q (input / period)]
(rect 1
| color (hsv (hash $i) 0.5 1)
| move (hash2 $i | remap- * 0.33)
| tile: $i [1 1] :oversample true
| shape/distance)))
(ball 100
| expound (fbm 3 squarley
[(atan2 p.xy | abs) (sqrt (150 - p.z | abs))]
[.5 1]) 20
| rotate y (t / 10)
)
&lt;/code>&lt;/pre>&lt;p>But so far this is not a very interesting effect. What if we vary the orientation as well?&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def animate (osc t 5 | ss 0.1 0.7))
(def size 60)
(def points
(rect size
| color (hsv (hash $i) 0.5 1)
| rotate (hash $i 1000 * pi/2 * animate)
| move (hash2 $i | remap- * animate * 10)
| tile: $i (0.5 * size | vec2) :oversample true
))
(set background-color
(vec3 (shape/distance points + size / (0.5 * size))))
(expand points (- 3 size))
&lt;/code>&lt;/pre>&lt;p>It&amp;rsquo;s a little bit more random-looking, I guess:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn squarley [input &amp;amp;opt period]
(default period 1)
(gl/with [q (input / period)]
(rect 1
| rotate (hash $i 1000 * pi/2)
| color (hsv (hash $i) 0.5 1)
| move (hash2 $i | remap- * 0.33)
| tile: $i [1 1] :oversample true
| shape/distance)))
(ball 100
| expound (fbm 3 squarley
[(atan2 p.xy | abs) (sqrt (150 - p.z | abs))]
[.5 1]) 20
| rotate y (t / 10)
)
&lt;/code>&lt;/pre>&lt;p>But distorting 3D space with 2D noise is not&amp;hellip; it doesn&amp;rsquo;t look great.&lt;/p>
&lt;p>Let&amp;rsquo;s jump to 3D.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def animate (osc t 5 | ss 0.1 0.7))
(def size 60)
(def points
(box size
| shade (hsv (hash $i) 0.5 1)
| rotate (hash3 $i 2000) (hash $i 1000 * pi/2 * animate)
| move (hash3 $i | remap- * animate * 10)
| tile: $i (0.5 * size | vec3) :limit 5 :oversample true
))
(union
(plane (- ray.direction)
| color (shape/distance points + size / (0.5 * size)))
(expand points (- 3 size)) | scale 2)
&lt;/code>&lt;/pre>&lt;p>It&amp;rsquo;s a lot harder to visualize the distance field in 3D. What you&amp;rsquo;re seeing there is the distance field at the plane that passes through the origin and faces towards the camera &amp;ndash; you can click and drag the camera around to take different slices of space. I know it&amp;rsquo;s not a great visualization, but the point is that this technique generalizes to 3D (even if it&amp;rsquo;s hard to imagine the distance field at every point in 3D space).&lt;/p>
&lt;p>Let&amp;rsquo;s see how this looks when we use it to distort a 3D shape:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn cubeley [input &amp;amp;opt period]
(default period 1)
(gl/with [p (input / period)]
(box 1
| rotate (hash3 $i 2000) (hash $i 1000 * pi/2)
| move (hash3 $i | remap- * 0.33)
| tile: $i [1 1 1] :oversample true
| shape/distance)))
(torus y 100 50
| expound (cubeley p 30) 20
| rotate y (t / 10))
&lt;/code>&lt;/pre>&lt;p>It&amp;rsquo;s kind of interesting? Definitely better than what we had before. Sort of a faceted gemstone effect.&lt;/p>
&lt;p>Do you think our computers will catch on fire if we try multiple octaves of this?&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn cubeley [input &amp;amp;opt period]
(default period 1)
(gl/with [p (input / period)]
(box 1
| rotate (hash3 $i 2000) (hash $i 1000 * pi/2)
| move (hash3 $i | remap- * 0.33)
| tile: $i [1 1 1] :oversample true
| shape/distance)))
(torus y 100 50
| expound (fbm 3 cubeley p 30) 20
| rotate y (t / 10))
&lt;/code>&lt;/pre>&lt;p>I&amp;rsquo;m glad you&amp;rsquo;re still with me.&lt;/p>
&lt;p>Let&amp;rsquo;s trade the boxes for cones:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn coneley [input &amp;amp;opt period]
(default period 1)
(gl/with [p (input / period)]
(cone y 1 1
| move (hash3 $i | remap- * 0.33)
| tile: $i [1 1 1] :oversample true
| shape/distance)))
(ball [100 150 100]
| expound (coneley p 30) 20
| rotate y (t / 10)
)
&lt;/code>&lt;/pre>&lt;p>It&amp;rsquo;s kind of an interesting pinecone-y texture? I guess?&lt;/p>
&lt;p>There are more primitives to try. But of course we don&amp;rsquo;t have to limit ourselves to primitive shapes.&lt;/p>
&lt;p>This is a classic SDF example to demonstrate how easy it is to do constructive solid geometry stuff:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(box 100 :r 10
| subtract :r 10 (sphere 120))
&lt;/code>&lt;/pre>&lt;p>What if&amp;hellip; we used &lt;em>that&lt;/em> as the basis for our Worley noise?&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def jitter (osc t 5 | ss 0.1 0.9))
(defn cubeley [input &amp;amp;opt period]
(default period 1)
(gl/with [p (input / period)]
(box 1 :r 0.1
| subtract :r 0.1 (sphere 1.2)
| move (hash3 $i | remap- * 0.33 * jitter)
| tile: $i [1 1 1] :oversample true
| shape/distance)))
(torus y 100 50
| expound (cubeley p 30) 20
| rotate y (t / 10))
&lt;/code>&lt;/pre>&lt;p>I think it&amp;rsquo;s kind of more interesting without the randomization.&lt;/p>
&lt;p>We&amp;rsquo;ve constructed an interesting 3D noise function, and we&amp;rsquo;re using it to distort 3D space. But of course, we can go back to considering this a &amp;ldquo;noise texture&amp;rdquo; in the original sense of the word:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def jitter (osc t 5 | ss 0.1 0.9))
(defn cubeley [input &amp;amp;opt period]
(default period 1)
(gl/with [p (input / period)]
(box 1 :r 0.1
| subtract :r 0.1 (sphere 1.2)
| rotate (hash3 $i 1000) (hash $i 2000 * jitter)
| move (hash3 $i | remap- * 0.33 * jitter)
| tile: $i [1 1 1] :oversample true :sample-from -2 :sample-to 2
| shape/distance)))
(r2 | color (vec3 (fbm 4 cubeley [q 0] 128 | abs)))
&lt;/code>&lt;/pre>&lt;p>Kinda neat.&lt;/p>
&lt;p>The point of all of this is: Worley noise invites us to reconsider signed distance functions as more than implicit surfaces. And since Bauble makes it easy to construct signed distance functions, it&amp;rsquo;s a good playground for experimenting with textures like this.&lt;/p>
&lt;p>Even if we never found anything particularly attractive, it&amp;rsquo;s fun to play around with space.&lt;/p>
&lt;p>If this is your first time seeing &lt;a href="https://ianthehenry.com/posts/bauble/building-bauble/">Bauble&lt;/a>, hey welcome! This post is an expansion of something &lt;a href="https://www.youtube.com/watch?v=XHNBRAgD4f4&amp;amp;t=2217">I briefly talked about in a YouTube video once&lt;/a>. The video has many more examples of the sorts of things that Bauble can do. Check it out if this piqued your interest!&lt;/p></description><pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate><link>https://ianthehenry.com/posts/generalized-worley-noise/</link><guid isPermaLink="true">https://ianthehenry.com/posts/generalized-worley-noise/</guid></item><item><title>Building Bauble</title><description>&lt;p>I made something that I think is pretty neat, and I want to tell you about it.&lt;/p>
&lt;a class="image-container" href="https://ianthehenry.com/posts/bauble/building-bauble/balloon_hu61af7d15d560861d79c4cc0f3adfa0ec_4858488_1536x1536_fit_box_3.png">&lt;picture>&lt;source type="image/webp"
srcset="https://ianthehenry.com/posts/bauble/building-bauble/balloon_hu61af7d15d560861d79c4cc0f3adfa0ec_4858488_768x768_fit_q75_h2_box_3.webp 768w, https://ianthehenry.com/posts/bauble/building-bauble/balloon_hu61af7d15d560861d79c4cc0f3adfa0ec_4858488_1536x1536_fit_q75_h2_box_3.webp 1536w, https://ianthehenry.com/posts/bauble/building-bauble/balloon_hu61af7d15d560861d79c4cc0f3adfa0ec_4858488_375x375_fit_q75_h2_box_3.webp 375w, https://ianthehenry.com/posts/bauble/building-bauble/balloon_hu61af7d15d560861d79c4cc0f3adfa0ec_4858488_750x750_fit_q75_h2_box_3.webp 750w"
sizes="(max-width: 400px) 375px, 768px">&lt;img
class=""
srcset="https://ianthehenry.com/posts/bauble/building-bauble/balloon_hu61af7d15d560861d79c4cc0f3adfa0ec_4858488_768x768_fit_box_3.png 768w, https://ianthehenry.com/posts/bauble/building-bauble/balloon_hu61af7d15d560861d79c4cc0f3adfa0ec_4858488_1536x1536_fit_box_3.png 1536w, https://ianthehenry.com/posts/bauble/building-bauble/balloon_hu61af7d15d560861d79c4cc0f3adfa0ec_4858488_375x375_fit_box_3.png 375w, https://ianthehenry.com/posts/bauble/building-bauble/balloon_hu61af7d15d560861d79c4cc0f3adfa0ec_4858488_750x750_fit_box_3.png 750w"
alt=""
title=""
sizes="(max-width: 400px) 375px, 768px"
width="768"
height="432"
style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAFCAIAAAD38zoCAAAAhUlEQVR4nATA0QnCQAwA0MslbaWlaoWKdAPncghn8c8ZHMIJBMEPnUAqXJVeLpf46HQ8mMwpfSXFqqSCwKMROno/bxwnvyDXtDF8SuG6rqwgH3&amp;#43;B59DvNtt&amp;#43;2XYrNXAOsyju13kcR1Uehu51f2QxNS9qcL2cRVgloU3iG0QEADP9BwAA//91sUJFIMeP4gAAAABJRU5ErkJggg==);"
/>&lt;/picture>&lt;/a>
&lt;p>This is a little hot air balloon made out of alternating layers of brass and bronze that stack together with these angled facets:&lt;/p>
&lt;a class="image-container" href="https://ianthehenry.com/posts/bauble/building-bauble/layers_hud04b4e21bf34736dfa3c4b5bc0350dcb_5728706_1536x1536_fit_box_3.png">&lt;picture>&lt;source type="image/webp"
srcset="https://ianthehenry.com/posts/bauble/building-bauble/layers_hud04b4e21bf34736dfa3c4b5bc0350dcb_5728706_768x768_fit_q75_h2_box_3.webp 768w, https://ianthehenry.com/posts/bauble/building-bauble/layers_hud04b4e21bf34736dfa3c4b5bc0350dcb_5728706_1536x1536_fit_q75_h2_box_3.webp 1536w, https://ianthehenry.com/posts/bauble/building-bauble/layers_hud04b4e21bf34736dfa3c4b5bc0350dcb_5728706_375x375_fit_q75_h2_box_3.webp 375w, https://ianthehenry.com/posts/bauble/building-bauble/layers_hud04b4e21bf34736dfa3c4b5bc0350dcb_5728706_750x750_fit_q75_h2_box_3.webp 750w"
sizes="(max-width: 400px) 375px, 768px">&lt;img
class=""
srcset="https://ianthehenry.com/posts/bauble/building-bauble/layers_hud04b4e21bf34736dfa3c4b5bc0350dcb_5728706_768x768_fit_box_3.png 768w, https://ianthehenry.com/posts/bauble/building-bauble/layers_hud04b4e21bf34736dfa3c4b5bc0350dcb_5728706_1536x1536_fit_box_3.png 1536w, https://ianthehenry.com/posts/bauble/building-bauble/layers_hud04b4e21bf34736dfa3c4b5bc0350dcb_5728706_375x375_fit_box_3.png 375w, https://ianthehenry.com/posts/bauble/building-bauble/layers_hud04b4e21bf34736dfa3c4b5bc0350dcb_5728706_750x750_fit_box_3.png 750w"
alt=""
title=""
sizes="(max-width: 400px) 375px, 768px"
width="768"
height="432"
style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAFCAIAAAD38zoCAAAAeklEQVR4nATAvQ6CMBAA4PvrYYuJDsKiYZPHdtKncjQx0dEByLW9&amp;#43;snjfoup105VA2Aj5JJLtizMnM1sW3Z9ZCZC8grNXUop39fz93k3tLpZPB5O4zROswDAcL4Ol7nrBNyhASJ6BWIOMe2XdRXWoCloZFZE&amp;#43;gcAAP//NMIrP8TPzEEAAAAASUVORK5CYII=);"
/>&lt;/picture>&lt;/a>
&lt;p>It&amp;rsquo;s 3D printed, sort of, but it really is solid metal &amp;ndash; it&amp;rsquo;s not a metallic filament. It&amp;rsquo;s made by &amp;ldquo;lost wax casting,&amp;rdquo; where you 3D print a model out of resin, then pack it in plaster, and then once the plaster dries you melt out the resin and fill the void with molten&amp;ndash;&lt;/p>
&lt;p>You know what? This is neat, but this actually isn&amp;rsquo;t what I wanted to tell you about.&lt;/p>
&lt;div style="display: flex; flex-direction: column; gap: 1rem; align-items: center;">
&lt;canvas style="display: block; border-radius: 4px; border: solid 2px var(--palette-blue); width: 100%; cursor: grab; aspect-ratio: 2/1;" id="expandy-balloon">&lt;/canvas>
&lt;input style="display: block; width: 50%;" type="range" autocomplete="off" value="0" min="0" max="30" step="0.01" />&lt;label style="display: block; width: 50%; margin: 0 auto;">
&lt;input autocomplete="off" type="checkbox" checked> Get cute with me&lt;/label>
&lt;/div>
&lt;p>Neither is that, but we&amp;rsquo;re getting closer.&lt;/p>
&lt;p>That&amp;rsquo;s the 3D model that the balloon is cast from. I didn&amp;rsquo;t actually cast the balloon &amp;ndash; I paid someone else to do that for me &amp;ndash; but I &lt;em>did&lt;/em> make the 3D model.&lt;/p>
&lt;p>And it&amp;rsquo;s an interesting 3D model. It&amp;rsquo;s not a triangle mesh, like most 3D shapes you encounter. It has no faces; it has no vertices. Instead, it&amp;rsquo;s made entirely out of math: this balloon is a pure function of 3D space.&lt;/p>
&lt;p>Here, take a look:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def thickness 25)
(def sections 12)
(def angle (pi * 0.25))
(def lobe-intensity 1)
(def bezel 1)
(def bronziness 1.5)
(def branzino false)
(ball [(100 / (lobe-intensity + 1)) 100 100]
| union :r 50 (cylinder y 25 50 | move [0 -100 0])
| scale y (ss p.y -100 100 1 0.8)
| intersect :r bezel
(plane y | shell thickness
| color (gl/if (mod $i.y 2 | = 0) (pow default-3d-color bronziness) default-3d-color)
| tile: $i [0 thickness 0]
| rotate z (remap- parity * angle)
| gl/let [parity (mod $i 2)] _)
| radial: $i y sections
| move y 40
)
&lt;/code>&lt;/pre>&lt;p>There&amp;rsquo;s the source code to that hot air balloon. Mess around with it. Edit some constants. Pull up the autocomplete with &lt;code>ctrl-space&lt;/code>, and see what else it can do.&lt;/p>
&lt;p>This is called &lt;a href="https://bauble.studio/">Bauble&lt;/a>, and &lt;em>this&lt;/em> is what I wanted to tell you about.&lt;/p>
&lt;p>Bauble is a tool &amp;ndash; toy? &amp;ndash; that I wrote in 2022, because I wanted to make pictures with math on my computer. And not just simple geometrical things like that. I wanted to make pictures like this:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn fork [shape f1 f2]
(union (f1 shape) (f2 shape)))
(defn spoon [shape f1 f2]
(union shape (f1 shape) (f2 shape)))
(defn cel [shape rgb]
(shade shape :f (fn [light] (gl/do
(var cel-shadow (step 0.8 light.brightness + 1 / 2))
(var cel-shading (dot light.direction normal * light.color | quantize 2 * cel-shadow))
(var regular-shading (dot light.direction normal * light.color * cel-shadow))
(var b (mix cel-shading regular-shading 0.5 + 0.5))
(b * rgb)))))
(setdyn *lights* [(light/directional 1 [-2 -2 -1] 1024 :shadow 0.25)])
(def ear
(cone y 40 153 :r 12
| morph 0.15 (sphere 46 | move y 64)
| union :r 13
(cylinder y 26 30 | move y -10)
| scale z 0.5))
(def ears
(ear
| move x 134
| rotate z (tau * -0.01)
| mirror x))
(def body
(ball [1 0.75 0.5 * 100]
| union :r 72 (ball [0.58 (.84 * 0.75) 0.5 * 250] | move y -156)))
(defn body-color [$] (cel $ (hsv (4 / 6) 0.1 0.3)))
(def decoration (rect [32 10] :r 10 | rotate (q.x * 0.044 - pi) | rotate pi))
(def top-decorations
(fork decoration
(fn [$] ($ | rotate -0.21))
(fn [$] ($ | scale 0.9 | move x 71 | rotate -0.30))
| move x 40
| mirror x))
(def bottom-decorations
(spoon decoration
(fn [$] ($ | scale 0.95 | move x 76 y 0 | rotate -0.14))
(fn [$] ($ | scale 0.9 | rotate -0.59 | move x 140 y -37))
| mirror x))
(def decorations (union top-decorations (bottom-decorations | move y -80)))
(def tummy-patch
(box 110 :r 64
| morph 0.70 (sphere 118)
| move z 60 y -137
| cel (hsl (/ 69 255) 0.10 0.65)))
(def body-and-ears
(union :r 6
body
(ears | scale 0.42 | move y 81)
| body-color
| union-color (subtract tummy-patch (decorations | extrude z inf | scale 0.5 | move y -46))))
(def eyes
( sphere 14
| union :r 4 (box [14 0 1] | move z 4)
| cel 10 | union-color (sphere 5 | move z 14 | cel 0.05)
| scale z 0.5
| rotate x -0.37 y 0.34
| move [52 28 43]
| mirror x))
(def arms
(box [15 100 (ss p.y -100 50 25 40)] :r 15
| rotate z (p.y * 0.002)
| rotate z 0.30
| move x 131 y -122
| mirror x
| body-color))
(def whiskers
(union
(line [0 0 0] [85 0 0] 1.5 0.5 | rotate z 0.03 | move [0 6 0])
(line [0 0 0] [82 0 0] 1.5 0.5 | rotate z -0.03)
(line [0 0 0] [87 0 0] 1.5 0.5 | rotate z -0.10 | move [0 -6 3])
| move [60 0 41]
| mirror x
| color 0.1))
(def floor (ground -300 | cel (hsv 0.7 0.1 0.04)))
(def nostrils
(cylinder z 2.5 3
| move [7 1 4]
| mirror x
| color [0 0 0]))
(def nose
(ball 11
| subtract :r 2 (cylinder z 10 10 | move x 11 y -12 | mirror x :r 1)
| scale z 0.6 y 0.7 x 1.5
| rotate x -0.5
| cel (hsv 0 0.0 0.02)
| union-color nostrils
| scale 1.20
| move z 48 y 30))
(def lilypad
(cylinder y (5 * sin (theta + 1 * 3) * cos (theta - 1 * 2) + 40) 0.3
| move y (sin (theta * 4) * cos (theta * 2 + (length p.xz / 8 + 18)) * dot p.xz * 0.005 + (ss p.z 0 40 0 -10))
| slow 0.7
| union :r 5 (cylinder y 1 10 :r 1 | move y 10 | rotate x 0.2 z (sin (p.y / 4) * 0.1))
| move y 77
| cel (hsv (2 / 6) 0.9 0.5)
| gl/let [theta (atan2 p.xz)] _))
(union
eyes
(union body-and-ears
arms :r (10 - (distance [(abs p.x) p.y p.z] [120 -78 -1] / 10) | max 0)
| expound (perlin p 15) 0.05 2)
whiskers
floor
nose
lilypad
| tint white (fresnel 5 * 0.3))
&lt;/code>&lt;/pre>&lt;p>I had just discovered &lt;em>signed distance functions&lt;/em>, and I was enamored by the power that they give you to sculpt space with simple mathematical expressions.&lt;/p>
&lt;p>Signed distance functions &amp;ndash; SDFs &amp;ndash; are amazing, and if this is the first time you&amp;rsquo;re hearing about them, you should probably drop everything you&amp;rsquo;re doing today and &lt;a href="https://www.youtube.com/watch?v=8--5LwHRhjk">watch this twenty-five minute video of Inigo Quilez using signed distance functions to create an animation&lt;/a> instead. Yes, twenty-five minutes is a lot of minutes. It&amp;rsquo;s worth it.&lt;/p>
&lt;p>I know you didn&amp;rsquo;t actually watch the video, but the overall gist is that someone very smart and very good at math describes an animation he created out of pure functions of time and space. But the description is pretty high-level: he says things like &amp;ldquo;&lt;a href="https://www.youtube.com/watch?v=8--5LwHRhjk&amp;amp;t=944s">we&amp;rsquo;ll define three circles that we spin as we move down the parameterization of the curve&lt;/a>,&amp;rdquo; which is a beautiful way to &lt;em>think about&lt;/em> the effect he uses to create the braids in that video &amp;ndash; but how do you actually &lt;em>do&lt;/em> that?&lt;/p>
&lt;p>Well, you write several hundred lines of something called GLSL, plumbing arguments around and looking up how to construct rotation matrices and forgetting that matrices are column-major in GLSL and trying to remember what you stuck in the &lt;code>w&lt;/code> component of this vector and, well, you can &lt;em>do it&lt;/em>, and &lt;a href="https://www.shadertoy.com/results?sort=newest">lots of people have&lt;/a>, but not without losing some of the mathematical elegance of the original, intuitive presentation.&lt;/p>
&lt;p>Because I really just want to write &amp;ldquo;gimme three circles extruded along a bezier curve, and rotate them by an angle that varies with the current position along the curve,&amp;rdquo; you know?&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(circle 10
| color (hsv ($i + 3 / 6) 0.6 1)
| radial: $i 3 5
| rotate ($t * tau * 4 + t)
| bezier: $t [-100 0 100] [0 100 0] [100 0 -100]
:to (osc t 5 0 1 | ss 0.1 1))
&lt;/code>&lt;/pre>&lt;p>(You can click to pause any of the animations on this page.)&lt;/p>
&lt;p>So: Bauble. I wrote Bauble to solve this impedance mismatch, so that I could play around with the SDFs the way that I wanted to play around with SDFs: in a functional, expression-oriented programming language. SDFs are signed distance &lt;em>functions&lt;/em>, remember, and primitive operations on SDFs like rotation or translation are literal &lt;em>function composition&lt;/em>. You can write a function that takes an SDF and an angle and returns a new SDF &amp;ndash; a new function &amp;ndash; for the rotated shape.&lt;/p>
&lt;p>But you can&amp;rsquo;t write that in GLSL! GLSL doesn&amp;rsquo;t have first-class functions, so you actually have to do this composition by hand: if you want to rotate a shape, you have to rotate its input coordinate first, then pass the newly-rotated point in space to the SDF. Which, like, is &lt;em>fine&lt;/em>, but it&amp;rsquo;s friction, and that&amp;rsquo;s the very simplest sort of operation &amp;ndash; once you get into more interesting higher-order operations like instanced tiling, the friction stops feeling fine.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(gl/def apothem (osc t 15 5 10))
(circle (oss t 7 (apothem * 0.5) (apothem * 2 / sqrt 3))
| shade (hsv (hash $i + (t / 10)) 0.75 0.8)
| with-lights (light/point 1 (P + normal))
| move x (mod $i.y 2 * apothem)
| tile: $i [(apothem * 2) (apothem * sqrt 3)] :oversample true
| revolve z | rotate x (t / 20) z (t / 5)
| intersect (cylinder x 150 20 :r 20) :r 2)
(set camera (camera/perspective [403 0 0] :fov 45))
&lt;/code>&lt;/pre>&lt;p>Bauble is not just a higher-level language, though. It&amp;rsquo;s more accurate to say that I started working on Bauble because I was frustrated with the speed at which I was able to write shaders using SDFs. Not just the verbose manual composition, but the verbose manual &lt;em>composition:&lt;/em> it&amp;rsquo;s hard to compose a detailed scene in pure code! I didn&amp;rsquo;t only want to make abstract &amp;ldquo;shadery&amp;rdquo; looking things. I wanted to be able to make characters too, and that requires a degree of precise and subjective control: I wanted to be able to drag things around, edit shapes interactively, see my shader update live, look at it from different angles&amp;hellip; but instead I was over here backspacing over a &lt;code>1.4&lt;/code>, typing &lt;code>1.5&lt;/code>, recompiling, and deciding if it looks better or worse.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble"># &amp;quot;manta raymarching&amp;quot;
(gl/def bg (ok/mix [0.1 0.1 0.25] sky (ray.direction.y | remap+)))
(triangle [(ss (q.y * gl/if (&amp;lt; q.x 0) 1 0) 0 65 65 40) 130] | rotate (q.y / 270)
| extrude y | expand (ss p.z 140 0 1 3)
| union :r 10 (cone x 18 -184 :r 10 | scale [1 1 2] | move x 49)
| shell 4
| subtract :r 5 (ball [30 30 30] | move x 75)
| union :r 10
(rect [11 5] :r [0 5 5 0]
| extrude z
| expand 1
| rotate x (p.y / -30)
| rotate x (p.x / 40) | pivot [-10 0 0]
| rotate y (p.x / 200 + (sin+ t * 0.5)) | pivot [-10 0 0]
| move x 70 z 28)
| scale [1 (ss p.x 0 -100 1 0.2) 1]
| union :r 3 (cone x 5 -100 | move x -45)
| shade (mix (blue * 0.03) [0.9 1 1]
(2000 / (distance (abs p.xz) [57 117] | pow 3) | clamp 0 1)
) :g 20
| mirror z
| union-color (
(shape/2d
(gl/if (&amp;gt; (hash ($i + 100) * (14 / length ($i * [2 1]))) 0.5)
(distance q (hash2 $i * 4) - (ss (hash $i) 0 1 1 1 * (ss normal.y 0.5 1)))
1000))
| tile: $i [4 4] :oversample true | shade white | extrude y inf)
| morph (shade r3 gray) :distance 0 :color (1 - occlusion :dist 40 | ss 0.4 0.5
+ (dot normal -y | max 0)
)
| union (ball 2 | move [61 -3 32] | mirror z | shade [0 0 0] :g 20 :s 1)
| rotate x (ss p.z 200 0 1 0 * osc t 3 pi/4 -pi/4) | pivot [0 0 10]
| rotate x (ss p.z -200 0 1 0 * osc (t + (sin t * 0.1)) 3 -pi/4 pi/4) | pivot [0 0 -10]
| rotate z (p.x / 800 * osc t 3 -1 1 + osc t 3 -0.1 0.1) | pivot [(oss t 6 -100 100) 0 0]
| rotate x (osc t 6 -0.1 0.1) y (osc t 12 -0.2 0.2)
| bound (ball 140 | move x -20) 20
| move (hash3 $i * 400 + [(osc t 3 -10 10) (oss t 6 -40 40) 0])
| gl/let [t (hash $i * 10 + t)] _
| tile: $i (vec3 700) :limit [50000 8 10] :oversample true
| move x (t * 150)
| map-color (fn [c] (mix c bg (depth / 5000 | pow 2 | clamp 0 1)))
| slow 0.8)
(set background-color bg)
&lt;/code>&lt;/pre>&lt;p>And the camera, gosh &amp;ndash; modeling in 3D with a fixed camera is &lt;em>hard&lt;/em>. And &amp;ndash; while I realize this sounds really dumb &amp;ndash; I think the math to calculate a perspective matrix and position a camera where you want pointed in the direction that you want is actually &lt;em>much harder&lt;/em> than any the math related to the actual SDFs that you&amp;rsquo;re trying to render.&lt;/p>
&lt;p>So I whipped up a little hack that would basically just concatenate strings of GLSL for me, and put them in a little window with a moving camera.&lt;/p>
&lt;p>It took me a few days to get it working: I decided that I wanted to use &lt;a href="https://janet-lang.org/">Janet&lt;/a> as my &amp;ldquo;high level&amp;rdquo; language, because &lt;a href="https://ianthehenry.com/posts/janet-game/">I&amp;rsquo;d had a positive experience with the language before&lt;/a>, and I knew that it could &lt;em>at least in theory&lt;/em> run inside the browser. I had never used WebAssembly before, and I had &lt;em>barely&lt;/em> used Janet at this point, and even following existing examples I had quite a time getting it to work. Nevermind that it had been almost a decade since I&amp;rsquo;d done web development seriously, and my sole experience with WebGL at that point was &lt;a href="https://ianthehenry.com/posts/delaunay/">making a few visuals for an old blog post&lt;/a>.&lt;/p>
&lt;p>But I got &lt;em>something&lt;/em> working, eventually. Here&amp;rsquo;s the very first demo I ever recorded of the thing that would become Bauble:&lt;/p>
&lt;div class="video-container">
&lt;video controls
width="1280"
height="505"
preload=metadata
poster="/posts/bauble/building-bauble/og-bauble-poster_huab5b6797bb69995ea66d2a9b7579c953_506490_2560x1010_fit_q75_h2_box_3.1e5abcfe55088f8db41cf468dc17fb09c31bc3129f31861034b76b02654f9960.webp"
style="
background-size: cover;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAANCAIAAABHKvtLAAACEklEQVR4nJxSPW8TQRDd3dld79zl4jj2QQKKRCBKg6kRQqKCNhEd&amp;#43;Qf8p1BS0fEDCBQoBQWgNDShSAQYJcbYF9/5vmYXmQgFG8uOecVqV/PmzXujlZubTSLSWhdFzrnQWllrk2SQZRmbH8YYnzNnUCqVUslL4u&amp;#43;ffigtWUcNrFfA/IfodPDWQYsx70fny8bGLQC4TM/rvb3d3WczaU92dra2t2RZK8Ow0j/Gb/nX/hkzRllL4ZWaVgY9ObGzj/FRcnR&amp;#43;d7xyPX/QV&amp;#43;8iVlwr7vf0fuzi81Kke7zBhHOOMXZjfS1LCSCXEsMwpBIE8MukyYta7dA&amp;#43;yhpxGd45pLtpfYww9Bj1CiGSqyshUWnJMM4FsE5ngCiDQAOIKQOU&amp;#43;g6rJ29xcZlBa/XNkSHmRpzJbrdbrfqCpwKAiKwVNnFKAhMu6iWcWaVHFD3P&amp;#43;/spHPvEt1c6Dx9bjmL/wD0fT4CIaJRjFgDyPEWEOO5L8AE0VrS1xWBQLi1Vp&amp;#43;Q4EdnNz68Sy&amp;#43;6tk&amp;#43;SidHZ8RVKJ09OfjUYd0WeM&amp;#43;/5CECz8Icz&amp;#43;uBI&amp;#43;&amp;#43;q6acnYGkR1VH1adcwA8inrO2SxVErzlOgbBTNkLYHDcut1es&amp;#43;6FX7ekxwcMD&amp;#43;eUUp6uVBcXGGdSXrggoqIojJmWQ5Js6ZWXQ/ak6nALqJrN5sRm&amp;#43;I054vwD3m63gyCYSyVN0ziOZ9I8z0PEXwEAAP//133WuvN4BLkAAAAASUVORK5CYII=);
">
&lt;source src="https://ianthehenry.com/posts/bauble/building-bauble/og-bauble-2560x1010.9b4b65e1ed3bc0668c240fb5ac62a21cafaaee7a90974c6199f0a48dda888d36.mp4" type="video/mp4">
&lt;/video>
&lt;/div>
&lt;p>Notice the dark, oversaturated colors. I didn&amp;rsquo;t know I had to do my own gamma correction! This was like my third ever shader. I had no idea what I was doing.&lt;/p>
&lt;p>But even though this was extremely crude &amp;ndash; it was literally GLSL string concatenation, of a few fixed primitive shapes, with no dynamic expressions of any kind &amp;ndash; it was already &lt;em>so much better&lt;/em> than writing GLSL by hand. Even just being able to type &lt;code>[1 2 3]&lt;/code> instead of &lt;code>vec3(1.0, 2.0, 3.0)&lt;/code> was worth the time I&amp;rsquo;d spent on it.&lt;/p>
&lt;p>And it was &lt;em>fun&lt;/em>. There&amp;rsquo;s something so viscerally satisfying about making something you can touch and play with and see in real time like this. I was having fun working on this little toy, so I kept working on it.&lt;/p>
&lt;p>I implemented an orbital camera. I switched the editor to &lt;a href="https://codemirror.net/">CodeMirror&lt;/a>, and learned how to write a Janet grammar for it, so that I could directly manipulate the parsed AST to edit values with my mouse (ctrl-click and drag on any number!). With CodeMirror came TypeScript, which I had never used before, and some cruel prank called &amp;ldquo;rollup,&amp;rdquo; and I got to experience firsthand the hell of the modern JavaScript ecosystem. I wrote a UI, and decided to try something called SolidJS, which I&amp;rsquo;ve mildly regretted ever since.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(union
(revolve shape y radius
| move y (atan2+ p.xz / tau * sep + (round (p.y / sep) * sep))
)
(revolve shape y radius
| move y (atan2+ p.xz / tau * sep + (round (p.y / sep) - 1 * sep))
)
| let [shape (circle 2
| shade (hsv (hash $i + hash $j + (t * 0.1)) 0.7 1) :s 1 :g 10
| with-lights (light/ambient 1 normal)
| tile: $i [10 10] :limit 4
| radial: $j 5 50
| rotate (t / 3))] _
| gl/let [radius (osc p.y 1000 50 200) sep 146] _
| rotate y t)
&lt;/code>&lt;/pre>&lt;p>Everything was very new and exciting, and I learned a lot about Wasm and Janet and OpenGL and SDFs and procedural art in general.&lt;/p>
&lt;p>And I kept growing the capabilities of Bauble&amp;rsquo;s&amp;hellip; compiler? Would we call it a compiler? It was still, at this point, a glorified string concatenator. But I taught it how to concatenate real fancy-like; I added support for custom dynamic expressions so that you could write things like &amp;ldquo;rotate space around the y-axis by an angle that varies with the current &lt;code>y&lt;/code> coordinate:&amp;rdquo;&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(star 100 50 | extrude y 100
| rotate y (osc t 3 | ss 0.1 0.9 * pi/2 * p.y / 100 + (0.5 * t))
| slow 0.5)
&lt;/code>&lt;/pre>&lt;p>Eventually I even implemented animations, and complex surface-blending operations, and higher-order bounding operations to improve rendering performance, and domain repetition, and, and&amp;hellip;&lt;/p>
&lt;p>And finally my crowning achievement: custom dynamic lighting, with raymarched soft shadows, which you could specify on a shape-by-shape basis, and whose properties could vary over time and space to produce complex, interesting effects.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def light-count 6)
(defn light [i]
(gl/def at (rotate [0 (osc t 5 20 200) 60] y (i / light-count * tau + t)))
(light/point (hsv (i / light-count) 1 1) at :shadow 0.25
:brightness (100 / (dot P at | abs | pow (osc t 3 0.7 1.2)) | min 2)))
(octahedron 20 :r 5 | rotate x (t + $i) y (t + $i) z (t + $i)
| shade (hsv $i 0.6 0.5)
| union (ground -40 | shade gray)
| with-lights ;(seq [i :range [0 light-count]] (light i))
| gl/let [$i (hash $i)] _
| tile: $i [80 0 80])
&lt;/code>&lt;/pre>&lt;p>It was the most complicated feature of Bauble, one that stretched its string concatenator to the absolute limits, one that had to be special-cased in the typechecker in order to generate correct code, and one that would still occasionally generate invalid GLSL if you looked at it wrong.&lt;/p>
&lt;p>It was also the last &amp;ldquo;must-have&amp;rdquo; feature. Once lighting was done, Bauble was &amp;ldquo;finished.&amp;rdquo; I wrote some token documentation, and a little tutorial, and I announced Bauble to the world. I forced myself to stop hacking on it for a little while, because I had more important things to do, and I went outside for the first time in two months.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(cylinder y 100 20 :r 20
| union (cone y 30 50 :r 5) :r 50
| expound (fbm 5 simplex [p.x p.z (distance p [0 50 0] - (t * 20) + (atan2+ p.xz / pi * -150) )] [50 50 20]) 3
| shade (mix blue sky 0.5) :g 30 :s 1
| slow 0.5
| tint sky (fresnel 5))
(set camera (camera/perspective [-180 100 0]))
&lt;/code>&lt;/pre>&lt;p>I didn&amp;rsquo;t set it aside for long. But when I returned to it, when I looked back over what I had wrought in this furious coding binge, I found&amp;hellip;&lt;/p>
&lt;p>You know that scene in &lt;em>Raiders of the Lost Ark&lt;/em> where they open up the roof of the Well of Souls, and they drop a torch down there, and the ground is just a solid mass of writhing snakes?&lt;/p>
&lt;p>That was basically the codebase that I had produced.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(morph 0.88
(ball 40 | move x 10 | rotate y (t * 2) | move y (osc t 20 -150 150))
(hexagon :r 5 10
| revolve x (80 + (40 * hash [$i $j]))
| shade (hsv (hash ($i + $j) * 0.04 - 0.03) 1 0.8) :g 15 :s 0.2
| radial: $j y 20 :oversample true :sample-from -1)
| rotate y (p.y / 40 | sin * (mod $i 2 * 2 - 1))
| rotate y (mod $i 2 * tau)
| radial: $i y 2 :oversample true :sample-from -1
| rotate y (t / 10)
| with-lights (light/directional white [-1 -2 0 | normalize] 300 :shadow 0.1)
(light/ambient 0.25 normal)
| slow 0.5)
&lt;/code>&lt;/pre>&lt;p>See, the string concatenation never just &lt;em>went away&lt;/em>. The whole core &amp;ldquo;compiler&amp;rdquo; was still based on this fragile web of carefully crafted, hardcoded GLSL primitives. There was never, at any point, an abstract syntax tree. There was a sort of weird builder-like imperative &amp;ldquo;code printer&amp;rdquo; &lt;em>thing&lt;/em> that &lt;em>sort of&lt;/em> implicitly tracked an AST and like knew what was in scope at some times, but, like, if you ever wrote a function with a local variable called &lt;code>p&lt;/code> you&amp;rsquo;d break everything, because &lt;code>p&lt;/code> is, obviously, the name of a dynamic variable that&amp;ndash;&lt;/p>
&lt;p>You know what? I don&amp;rsquo;t need to explain it. I&amp;rsquo;m sure you can believe me when I say that it was awful code.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(color r2 (teal * 0.1 *
(fbm 8 :f (fn [q] (rotate (q * 2) (pi * sin (t / 100))))
(fn [q] (cos q.x + sin q.y /)) q (osc t 20 30 90))))
(set aa-grid-size 2)
&lt;/code>&lt;/pre>&lt;p>But it wasn&amp;rsquo;t just the code. It was also a bad &lt;em>product&lt;/em>. It was too limiting: Bauble was a tool for making shaders with SDFs, but it didn&amp;rsquo;t give you any way to actually write your own signed distance functions. You just couldn&amp;rsquo;t write arbitrary shader code. You could write some custom &lt;em>expressions&lt;/em>, using a limited subset of the functions available to you in real GLSL, but there was no way that you, as a Bauble user, could have implemented any of the provided built-ins. There was no &amp;ldquo;escape hatch&amp;rdquo; to pure GLSL.&lt;/p>
&lt;p>So you were limited to the built-ins, and there just wasn&amp;rsquo;t that much built-into it. It was missing so many things: I wanted 2D SDFs, and extrusions into 3D space. I wanted to be able to distort normals without altering distance fields. I wanted to be able to define custom material shaders that could use Bauble&amp;rsquo;s native shadow casting &amp;ndash; you could define custom &lt;em>colors&lt;/em> of course, but the only light-aware material in all of Bauble was a simple Blinn-Phong shader. And that fact was, of course, hardcoded.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn strip [axis q]
(revolve (trapezoid (mix lo hi h) (mix hi lo h) 20 :r 2 | rotate (h * pi) t
| gl/let [lo 0 hi 10 h (atan2+ q / tau)] _) axis 100))
(union
(strip y p.xz | move x -50 | shade sky)
(strip z p.xy | move x 50 | shade orange)
| rotate z (t / 3) y (t / 2)
| tint purple (fresnel 5 * 0.5))
&lt;/code>&lt;/pre>&lt;p>Maybe more than anything else, I wanted to add 3D mesh export &amp;ndash; I wanted to be able to export Bauble shapes into OBJ files or STL files or whatever the right one is today, because I wanted to 3D print my Baubles. But I also wanted to add custom camera support, and anti-aliasing, and video export&amp;hellip;&lt;/p>
&lt;p>But I had stretched my strings to the breaking point. Even I couldn&amp;rsquo;t understand what I&amp;rsquo;d written, and I knew that, if I wanted to keep growing Bauble, I would have to rewrite the core compiler from scratch.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(gl/let [t1 (t / 4)
t2 (ss (fract t1) 0.2 1 (floor t1) (ceil t1))]
(defn nudge [i]
(hash3 i - 0.5
| normalize
| rotate y (osc t2 4 -0.5 0.5) z (osc t2 3 -0.5 0.5) x (osc t2 2 -0.5 0.5)))
(intersect :r (s * 20 + 1)
(plane [+1 +1 +1 + nudge 0 | normalize] 80)
(plane [+1 +1 -1 + nudge 1 | normalize] 80)
(plane [+1 -1 +1 + nudge 2 | normalize] 80)
(plane [+1 -1 -1 + nudge 3 | normalize] 80)
(plane [-1 +1 +1 + nudge 4 | normalize] 80)
(plane [-1 +1 -1 + nudge 5 | normalize] 80)
(plane [-1 -1 +1 + nudge 6 | normalize] 80)
(plane [-1 -1 -1 + nudge 7 | normalize] 80)
| expound (perlin p (20 * s + 30)) (20 * s) 20
| shade (ok/hcl (t2 * 0.4) 0.4 0.6) | with-lights (light/ambient 1 normal)
| gl/let [s (osc t2 1 0 1)] _
| rotate [1 -1 -1 | normalize] (t / 10)
| tint normal+ (fresnel 3)
| tint white (fresnel 0.5 * 0.3)
| map-color (fn [c] (c * (mix 0.1 1 (dot normal [-1 1 1 | normalize] | max 0))))))
&lt;/code>&lt;/pre>&lt;p>Fortunately though, over the course of Bauble&amp;rsquo;s development, I had produced a comprehensive suite of test scripts with reference images that demonstrated all of the edge cases and problems that I had faced and already fixed and&amp;hellip;&lt;/p>
&lt;p>No, of course not. I can&amp;rsquo;t even type that with straight fingers. There were no tests. Actually, worse: there was &lt;em>one&lt;/em> test. And it was failing.&lt;/p>
&lt;p>I tried to fix it, when I finally noticed it was broken. I tried to reverse engineer my own code, untangle my spaghetti mess to figure out how it had ever worked in the first place, but eventually I gave up. It just wasn&amp;rsquo;t worth it. There was nothing worth salvaging, and the thought of starting over from scratch after all of the work I&amp;rsquo;d already done was so discouraging that I just stopped working on Bauble altogether.&lt;/p>
&lt;p>And that&amp;rsquo;s the story of Bauble. It&amp;rsquo;s a sad story, a story of a codebase collapsing under its own weight, of a prototype trying to grow into a product, and finding that the old aphorism still holds true.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(defn half-hour [offset]
(capsule y 300 (ss p.y 0 300 0 100) (ss p.y 0 300 80 100)
| expound (perlin [0 (t + 2 | log * 300 + offset) 0 + p] 50) 20 10
))
(defn cel [shape color1 color2]
(color shape (mix color1 color2 (fresnel 5 | quantize 3))
| tint (vec3 -0.3) (fresnel 1 | quantize 3)))
(union :r 10
(half-hour 0 | move y 4 | cel sky white)
(gl/with [p [1 -1 1 * p]] (half-hour 1000 | move y 0 | cel orange red))
| scale [1 0.5 1])
(set camera (camera/perspective [0 100 400]))
&lt;/code>&lt;/pre>&lt;p style="margin-top: 60vh; margin-bottom: 60vh;">Two years passed.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(color r2
(ok/mix (ok/hcl (length q | ss 0 150 0.2 0.4 + (t / 30)) (length q / 200 | ss 0 1 0.3 0.1) 0.9)
(vec3 0.1)
(fbm 3 perlin (normalize [q 10] * (ss t 0 20 0 20 + t)) [(vec2 (length q | sqrt)) 6]
+ (length q / 150)))
| rotate (t / 20))
&lt;/code>&lt;/pre>&lt;p>I used Bauble on and off, but found myself increasingly annoyed by its limitations. Occasionally I would even try adding new features, but I could barely type through my hazmat suit.&lt;/p>
&lt;p>I kept meaning to write a blog post about Bauble, about everything that I&amp;rsquo;d learned &amp;ndash; how to embed Janet into a website and make an interactive art project that doesn&amp;rsquo;t use JavaScript &amp;ndash; but I never got around to it. Meanwhile I wrote &lt;a href="https://janet.guide/">a book about Janet&lt;/a>, and &lt;a href="https://janet.guide/embedding-janet/">dedicated a chapter&lt;/a> to my embedding experience, but it never even mentions Bauble.&lt;/p>
&lt;p>Despite being the most interesting side project that I&amp;rsquo;ve ever worked on, I haven&amp;rsquo;t written anything about it until now.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(gl/def sun-dir [0 -0.5 -1 | normalize])
(defn broad-height [xz] (perlin xz 10000 * 2000))
(gl/def sky-color (mix [1 0.5 1] [0 0 0] (ray.direction.y * 2) * 0.5
| mix (hsv (1 / 6) 0.5 1) (dot ray.direction (- sun-dir) | clamp 0 1)))
(plane y (perlin p.xz 600 * 200 + broad-height P.xz)
| expound (osc p.y 30) 10
| shade (hsv (0.6 / 6) 0.6 0.5 + (normal.yzx * 0.1)) :g (normal.y * 10) :s 0.1
| with-lights (light/directional 1 sun-dir 500 :shadow 0.5) (light/ambient (hsv 0.3 0.3 0.5) normal)
| map-color (fn [c] (mix c sky-color (1 - exp (* -0.0001 depth)) | pow [1.5 2 1.5]))
| slow 0.9)
(set background-color sky-color)
(gl/def camera-xz [(t * -500) 0])
(set camera (camera/perspective [camera-xz.x (broad-height camera-xz + 500) camera-xz.y] :dir [-1 -0.5 -0.10 | normalize]
| camera/tilt (osc t 20 -0.1 0.1)
| camera/pan (osc t 30 -0.1 0.1)))
&lt;/code>&lt;/pre>&lt;p>I&amp;rsquo;m writing about it now because I recently took a few months off work, for the usual reason, and after some weeks of sleep deprivation and exhaustion and elation I found that I had a few cycles to spend on a side project.&lt;/p>
&lt;p>But not a &lt;em>hard&lt;/em> side project. Not something that required concentration, or prolonged stretches of focus &amp;ndash; luxuries that I have temporarily foresworn. I needed something that I could, almost literally, do in my sleep.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(torus y 100 50
| expound (fbm 6 simplex p [100 50 100])
(osc t 10 | ss 0.25 0.75 | mix 1 (simplex+ (p + 1000 + (t * 10)) 300 | ss 0.2 1 * 15 + 1) _)
16
| shade 0.8 :g 40 :s 0.5
| rotate y (t / 2.5)
| slow 0.4
| tint [1 0.5 0.5] (fresnel 0.25 * 0.1)
| tint white (fresnel 5 * 0.1))
(set camera (camera/perspective [0 250 300 | rotate y (t / 2)]))
&lt;/code>&lt;/pre>&lt;p>So I rewrote Bauble. Or rather, I &lt;em>didn&amp;rsquo;t&lt;/em> rewrite Bauble &amp;ndash; instead, I did all of the boring things that I never bothered with the first time around. I wrote a GLSL AST library, with a little pretty-printer. I wrote a typed expression-oriented language that adds &amp;ldquo;first-class&amp;rdquo; functions to GLSL. I wrote a Janet DSL for constructing programs in this high-level language, and I added Janet wrappers for (&lt;em>almost&lt;/em>) all of GLSL&amp;rsquo;s built-in functions. I made a command-line interface to Bauble, using vanilla OpenGL instead of WebGL, so that I could finally write a real test suite.&lt;/p>
&lt;p>I wrote a real test suite.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def mouth (ball 50 | subtract (ball 200 | move y 200) | move z 100 | shade black))
(defn fork [shape f1 f2]
(union (f1 shape) (f2 shape)))
(def teeth
(box :r 2 [5 10 2]
| rotate x 0.30
| move y -40
| fork
(fn [$] ($ | move x 6))
(fn [$] ($ | rotate x -0.04 | move x -6 z -1))
| shade white :g 10 :s 1 )) # :ambient 0.5
(def eye-center [39 265 41])
(def eyelid
(ball 40
| move eye-center
| subtract :r 10
(ball 30 | move eye-center | move [0 -10 16])))
(def eye-color
(shade r3 white :g 10 :s 1 # :ambient 0.3
| union (ball 10 | move z 30 | shade black :g 10 :s 1)))
(def eyeball
(ball 35
| shade white :g 10 :s 1
| union-color (ball 10 | move [0 0 30] | shade black :g 10 :s 1)
| rotate x 0.34 y (sin t | ss 0 0.1 * 0.2 - 0.1)
| move eye-center))
(def neck
(cylinder y 30 100
| move y 100
| rotate z (p.y * -0.001)
| rotate x (p.y * 0.0010)
| move y 85
| union :r 10 eyelid))
(def head (ball 100 | scale y 0.9))
(def feet (box [20 30 20] :r 15
| union :r 15 (box :r 10 [10 20 30]
| fork
(fn [$] ($ | rotate y 0.1 | move x 8))
(fn [$] ($ | rotate y -0.1 | move x -11))
| rotate x 0.015 y 0.37
| move [15 -31 22])
| move [50 -82 0]
| rotate y 0.12
| subtract :r 5 (plane y -110)
| mirror x))
(union
(union :r 60 head neck | union :r 10 feet | shade green :s 0.5 :g 6 | tint white (fresnel 15 * 0.5) | subtract :s 2 mouth)
eyeball
(teeth | move z 80)
(ground -110 | shade gray)
| scale 0.5)
&lt;/code>&lt;/pre>&lt;p>I worked bottom-up this time, building one boring primitive at a time and stacking them on top of each other. It was not the joyful exploratory everything-is-new interactive process of building Bauble for the first time, but it was still rewarding: I could see where it was going, and how to get there. It was delayed gratification this time, knowing that if I just got through the slog of the rewrite, I would be rewarded with something that I could be proud of.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(union :r 10
(cone y 120 200)
(cone y 100 150 | move [80 0 -44])
(cone y 89 137 | move [-110 0 -9])
| expound (fbm :f 2.2 5 perlin [2 1 2 * p] 80) 20 40
| slow 0.5
| shade (normal+ | rotate y 1.34 | pow 2)
| move y (osc t 10 | ss 0 0.8 -230 -50)
| union :s 20 (plane y (osc (perlin [1 2 * p.xz] 200 + (t * 0.5)) 1 0 10) | shade sky :g 20 :s 1 | tint (fresnel 3))
)
&lt;/code>&lt;/pre>&lt;p>And I am, now. This is a new Bauble, and it is, across every axis, a better Bauble.&lt;/p>
&lt;p>You&amp;rsquo;ve already seen it of course, but let me give you a quick tour of what you can do with it now.&lt;/p>
&lt;p>You can edit complicated shaders without lag: Bauble uses &amp;ldquo;web workers&amp;rdquo; now, so that all of the Janet evaluation and compilation and rendering takes place off of the UI thread. This&amp;hellip; this doesn&amp;rsquo;t actually seem to work very well in Chrome or Safari, at least on my &amp;ldquo;Apple Silicon&amp;rdquo; MacBook &amp;ndash; recompilation is pretty stuttery, taking around 100ms with both the OpenGL and Metal backends. But it&amp;rsquo;s buttery smooth in Firefox. Weird. WebGL rendering performance is also just universally better in Firefox &amp;ndash; if any of the examples on this page are dipping below 60fps, maybe try switching?&lt;/p>
&lt;p>You can export 3D models, and you can 3D print them:&lt;/p>
&lt;a class="image-container" href="https://ianthehenry.com/posts/bauble/building-bauble/prints_hu39220f391f1ca3dd36179dd7d6b84420_6987945_1536x1536_fit_box_3.png">&lt;picture>&lt;source type="image/webp"
srcset="https://ianthehenry.com/posts/bauble/building-bauble/prints_hu39220f391f1ca3dd36179dd7d6b84420_6987945_768x768_fit_q75_h2_box_3.webp 768w, https://ianthehenry.com/posts/bauble/building-bauble/prints_hu39220f391f1ca3dd36179dd7d6b84420_6987945_1536x1536_fit_q75_h2_box_3.webp 1536w, https://ianthehenry.com/posts/bauble/building-bauble/prints_hu39220f391f1ca3dd36179dd7d6b84420_6987945_375x375_fit_q75_h2_box_3.webp 375w, https://ianthehenry.com/posts/bauble/building-bauble/prints_hu39220f391f1ca3dd36179dd7d6b84420_6987945_750x750_fit_q75_h2_box_3.webp 750w"
sizes="(max-width: 400px) 375px, 768px">&lt;img
class=""
srcset="https://ianthehenry.com/posts/bauble/building-bauble/prints_hu39220f391f1ca3dd36179dd7d6b84420_6987945_768x768_fit_box_3.png 768w, https://ianthehenry.com/posts/bauble/building-bauble/prints_hu39220f391f1ca3dd36179dd7d6b84420_6987945_1536x1536_fit_box_3.png 1536w, https://ianthehenry.com/posts/bauble/building-bauble/prints_hu39220f391f1ca3dd36179dd7d6b84420_6987945_375x375_fit_box_3.png 375w, https://ianthehenry.com/posts/bauble/building-bauble/prints_hu39220f391f1ca3dd36179dd7d6b84420_6987945_750x750_fit_box_3.png 750w"
alt=""
title=""
sizes="(max-width: 400px) 375px, 768px"
width="768"
height="386"
style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAECAIAAAA8r&amp;#43;mnAAAAcElEQVR4nGLpLYphZmFhZPj7j&amp;#43;EfKxsHKxsnOwcPEysHY7oZP4eA4I&amp;#43;Pb2TVVL99//rn83deYRlmVh4WUWW9v///cAlIMHNw8nAJMgv&amp;#43;Zfr/l1NMnsU&amp;#43;LJ6RkZGRiZGZiZGBiYmRAUQyMDEDAgAA//9SUBcRnp5aAgAAAABJRU5ErkJggg==);"
/>&lt;/picture>&lt;/a>
&lt;p>That shape on the left is called a &amp;ldquo;gyroid,&amp;rdquo; and it was the first Bauble that I ever 3D printed (or, well, had someone cast in bronze for me). There&amp;rsquo;s no gyroid primitive in Bauble, but you can create custom shapes by writing out an implicit function directly:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(def gyroid (shape/3d (gl/with [p (p / 15)]
(dot (cos p) (sin p.yzx)) + 1 * 10)))
(intersect :r 2.5 gyroid (ball 145))
# Er okay so this is not a &amp;quot;real&amp;quot; gyroid;
# it's more like a half-filled gyroid,
# because it's hard to print thin walls.
# The real deal looks like this:
# (def gyroid (shape/3d (gl/with [p (p / 15)]
# (dot (cos p) (sin p.yzx)) * 10)))
#
# (intersect :r 2.5 (gyroid | shell 1) (ball 145))
&lt;/code>&lt;/pre>&lt;p>I don&amp;rsquo;t have a 3D printer, and this feature is pretty new, so I haven&amp;rsquo;t really explored this very much yet. Also Bauble&amp;rsquo;s mesh export is&amp;hellip; primitive, to say the least. It&amp;rsquo;s just marching cubes, which means you have to generate pretty large models if you want to preserve fine details. I realize that there are many better algorithms for triangulating SDFs, and Bauble should probably use one of them. But&amp;hellip; I can only code for a few minutes a week now, and I spent my whole budget on the next feature.&lt;/p>
&lt;p>You can embed Bauble on other pages. Not the way that I&amp;rsquo;ve been doing &amp;ndash; the crimes I committed to Bauble&amp;rsquo;s build system in order to embed the editor here are not really replicable. But you can export your shaders to GLSL, and embed them on any page to add interactive 3D examples in a few lines of code. No one even needs to know you&amp;rsquo;re using Bauble:&lt;/p>
&lt;div style="display: flex; flex-direction: column; gap: 1rem; align-items: center;">
&lt;h2>How do planet work?&lt;/h2>
&lt;canvas style="image-rendering: pixelated; display: block; width: 50%; cursor: grab; aspect-ratio: 1/1;" id="planet">&lt;/canvas>
&lt;label style="display: flex; width: 50%; align-items: center; gap: 0.5rem;">&lt;span>Innard&lt;/span>
&lt;input style="flex: 1;" type="range" autocomplete="off" value="0" min="0" max="100" step="0.01" />
&lt;/label>
&lt;/div>
&lt;p>Since Bauble pre-compiles the shader, the actual &amp;ldquo;&lt;code>bauble.js&lt;/code>&amp;rdquo; that you have to embed is just a single 8kb pure-JS file.&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> You don&amp;rsquo;t need to include the Janet compiler or WebAssembly or anything fancy like that &amp;ndash; in fact, you don&amp;rsquo;t even have to use the Bauble library at all. You can construct the graphics context and compile the shader and draw it yourself, if you&amp;rsquo;d like.&lt;/p>
&lt;p>There&amp;rsquo;s a biannual event called the &amp;ldquo;&lt;a href="https://itch.io/jam/autumn-lisp-game-jam-2024">lisp game jam&lt;/a>,&amp;rdquo; and I think it would be fun to use Bauble to render the graphics for a game. Janet has &lt;a href="https://github.com/janet-lang/jaylib">pretty good bindings&lt;/a> to &lt;a href="https://www.raylib.com/">Raylib&lt;/a>, and you could use that to handle the input and sounds, but render all the graphics with Bauble.&lt;/p>
&lt;p>Here, click on this, and then move around with WASD:&lt;/p>
&lt;p>&lt;canvas style="image-rendering: pixelated; display: block; width: 75%;aspect-ratio: 3/2; margin: 0 auto;" id="game" tabindex="-1">&lt;/canvas>&lt;/p>
&lt;p>Obviously that&amp;rsquo;s not&amp;hellip; a game, exactly. There&amp;rsquo;s no hit detection, and fire &lt;em>probably&lt;/em> shouldn&amp;rsquo;t cast shadows. But, you know, that&amp;rsquo;s 30 lines of Bauble code plus 40 lines of JS for the event handling? Imagine what you could do if you weren&amp;rsquo;t furiously trying to finish the blog post you started writing months ago.&lt;/p>
&lt;p>One of my favorite new features of Bauble is that you can edit vectors interactively. Not just the ctrl-click-and-drag on scalars that I mentioned already, but actual dragging vectors around in 3D. Here: put your cursor inside the &lt;code>[50 100 150]&lt;/code>, and then open quad view with &lt;code>alt-q&lt;/code>. You should see crosshairs, and then you can cmd- or ctrl-click and drag one of the orthographic viewports to edit the vector with your mouse. Try it on the &lt;code>[0 0 0]&lt;/code> too, to move the box around!&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-bauble" data-lang="bauble">(box [50 100 150]
| move [0 0 0])
&lt;/code>&lt;/pre>&lt;p>So that&amp;rsquo;s everything you can do with Bauble.&lt;/p>
&lt;p>Except&amp;hellip; it&amp;rsquo;s not, is it? I just listed all of the things that &lt;em>I&lt;/em> can do with Bauble. Because I know some things about SDFs, and I know Janet, and I understand this weird DSL that I&amp;rsquo;ve created. But &lt;em>you&lt;/em> don&amp;rsquo;t &amp;ndash; yet. How could you?&lt;/p>
&lt;p>Which brings me to the last, biggest, and most important new feature of Bauble:&lt;/p>
&lt;iframe loading="lazy" style="border: solid 2px var(--palette-purple); border-radius: 4px; transform: scale(90%); width: 100%; aspect-ratio: 2/1;" src="https://bauble.studio/help">&lt;/iframe>
&lt;p>This is &lt;a href="https://bauble.studio/help">https://bauble.studio/help&lt;/a>. I wrote a giant reference page with hundreds of interactive examples of every primitive and operation and &lt;em>thing&lt;/em> that you can possibly do with Bauble. And it&amp;rsquo;s available right in the editor, any time you trigger autocomplete: the reference page and completions are both generated from the docstrings of the actual Janet functions. And in case the docstrings aren&amp;rsquo;t sufficient, there is a little &lt;code>source&lt;/code> link next to every single definition that will take you straight to the code.&lt;/p>
&lt;p>The documentation isn&amp;rsquo;t perfect: some small helpers are missing examples; the &amp;ldquo;escape hatch&amp;rdquo; to writing raw GLSL isn&amp;rsquo;t really described at all, and it doesn&amp;rsquo;t include any of the functions that Bauble lifts directly from GLSL. And I fully realize that a reference like this is no substitute for a decent tutorial.&lt;/p>
&lt;p>Bauble still needs a proper tutorial, and one day I&amp;rsquo;ll write it.&lt;/p>
&lt;p>One day I&amp;rsquo;ll write the Book of Bauble, and explain SDFs and procedural noise and periodic distortions of space and all the tricks that I&amp;rsquo;ve learned, and how you can apply them to Bauble.&lt;/p>
&lt;p>One day.&lt;/p>
&lt;p>Let&amp;rsquo;s say&amp;hellip; eighteen years from now, just to be on the safe side.&lt;/p>
&lt;div class="video-container">
&lt;video controls
width="960"
height="540"
preload=metadata
poster="/posts/bauble/building-bauble/bauble-top-poster_hu0cdfde228f2b3c9d9e835d2e5542a33d_2300086_1920x1080_fit_q75_h2_box_3.accc3b5cc27c42d1cbe49a17176aaebfe83487095e3f74bca999d90136acbfef.webp"
style="
background-size: cover;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAASCAIAAAC1qksFAAAD3ElEQVR4nFRVQW8cRROtqu6Z2Xi9TqKs186nfII4Qk6EQIIo4oq4IUXwCzgA4sCJEz8g/4ELR64cQUIcQEiAcooMFySIN3ZIZNnGXsfr3dmd3e6qQt09szY9s3OYqa736r3qWrvd/wtAFRQUEaFZmK7T/adbX385PBpMxtV0Ptt8867naTkaVzNPlkxuR8Oz/qOHS63lbu/ly9e6ndXuO59&amp;#43;TsaKeFUQZSvCEBD0PDXAAmp59Ua7u1qeDFX80pIt2h0ukWUkIuq1t3Hnvc8&amp;#43;&amp;#43;uPXH/786dscsdXu2IxERdmFgLDEPn70CyKySsip4UYiQkUim1kALK52i/ZeVpYOZu2VpWo0Rp8jWgT3/9uvn&amp;#43;zvrm/cvHr9k8c/fzcfDW/cfWt4tB8YJ55k8MHH91VYhDNSJGHvDCkog0qAQSW0165cP9593t/u3//wg3JYTcqqmk3HZy8odwrq/dx5X80qRHLOW5sBoAh7dsYYq&amp;#43;yR0JLGMljY&amp;#43;zkDMKqo&amp;#43;oABMFBZ6RZXBnZw2B8Pxp4NQFHk1kHFrEkNBGIflWMPQQ1UQM9ip&amp;#43;UpERBKMEPZUBAIEYhAWAHEeVe9KKn3v/XNW3s72wd7J87jrc3bNrehQhRmJoRWkTkvxBhbJqQO30TsfDYiRFVGQEOgJj6j7wjg/Mw7Z/P83rvvt1dWDp7ujE5P/n7Sb3VWjp89AwihROi9qIpj5QDnRdgYgxQUsvsHT4IXprDGZjYjDPWpcizcI4K1Zq3XGxwdf//Nj53LrdG43HhlM7d8uPtcQUWAg4Ximb1XEV&amp;#43;WA3aVsa286CAofvXFAwXIiyJR1qCLgEJUDEQcIHSWlwz/czYeVSVnubm0HDyo5tc0EWEW8Z4jjHNVNfFuCkgINkj06r23ISgehA8rmBMaOD40OhhyTI63yB74djmfTc9Oq9WX7nTaa/VHbVY6sfEXTQZhtlmeh6zRd0SkeMTiqUtx0Q6RrPfGpPxtcLgznZr1m69d6qwVrXaWF8bYumcSGY31sA/lxOLw962HuCAf5I/&amp;#43;JjSosVQFwl1zTAceiRoq9blKtFJQgAot76z3Dhf866zYTKPzHc2USjMlCikC9USpAxvkOA0CXUNk7HxWpbf1DMIGrt6kF3PELHEKImmKuTAgL0Sk4sJlz04HFIsNBgSrqa7iPzs1TqoFdPyWgiN40rTZ1WyMb&amp;#43;xkPMKmgyJEAtBFaDAg1FEnUFE0ZhEPTcULRgoLB8NLW1VTOgdAavZg/Y/Q2FxrFenTxexRbQqTp64Aa5DEz7r5PJFGIqyFMo0sdO5e7XnShJqODtGp/xplEC5YCgD/BgAA//9Ne3L9TKTXgQAAAABJRU5ErkJggg==);
">
&lt;source src="https://ianthehenry.com/posts/bauble/building-bauble/bauble-top-1920x1080.b77e3a67b7a172ae635e7653bf114ed4dbab96de2f4d23e33f02524ddda6d6ea.mp4" type="video/mp4">
&lt;/video>
&lt;/div>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>This is a little misleading, because the compiled shaders themselves are like 5-15kb each. I could minify their source, which would help a bit, but even without doing that, a single Bauble and the player library clocks in around 2% the size of embedding &lt;code>p5.js&lt;/code>. (Comparing minified, uncompressed sizes, which is, again, misleading.)&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><pubDate>Fri, 10 Jan 2025 00:00:00 +0000</pubDate><link>https://ianthehenry.com/posts/bauble/building-bauble/</link><guid isPermaLink="true">https://ianthehenry.com/posts/bauble/building-bauble/</guid></item><item><title>Quote-unquote "macros"</title><description>&lt;p>You&amp;rsquo;ve probably seen this Python 101 thing before:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="nd">@memoized&lt;/span>
&lt;span class="k">def&lt;/span> &lt;span class="nf">fib&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">n&lt;/span>&lt;span class="p">):&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="n">n&lt;/span> &lt;span class="o">&amp;lt;=&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">:&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="n">n&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="n">fib&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">n&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">fib&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">n&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Leaving aside &lt;a href="https://ianthehenry.com/posts/fibonacci/">the absurdity of computing Fibonacci numbers recursively&lt;/a>, it&amp;rsquo;s a common first introduction to Python &lt;a href="https://docs.python.org/3/glossary.html#term-decorator">decorators&lt;/a> and higher-order functions. &lt;code>fib&lt;/code> is just a function, and &lt;code>memoized&lt;/code> takes that function and returns a &lt;em>new&lt;/em> function (or something with a &lt;code>__call__&lt;/code> method) that, you know, memoizes the result.&lt;/p>
&lt;p>Python&amp;rsquo;s decorators give us a nice &lt;em>notation&lt;/em> for writing this, but we could write the same thing in any dynamic language with first-class functions. Like JavaScript:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="kr">const&lt;/span> &lt;span class="nx">memoized&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">f&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">results&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Map&lt;/span>&lt;span class="p">();&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">x&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">results&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">has&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">x&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">results&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">x&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">f&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">x&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">results&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">result&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">result&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kd">let&lt;/span> &lt;span class="nx">fib&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">n&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">n&lt;/span> &lt;span class="o">&amp;lt;=&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">n&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">fib&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">n&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">fib&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">n&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="nx">fib&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">memoized&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fib&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>And we&amp;rsquo;ll just gloss over, like, value equality and variadic functions and the memory leak this will cause.&lt;/p>
&lt;p>Is this &lt;em>useful?&lt;/em> Probably not, but that&amp;rsquo;s not the point. The point is that it&amp;rsquo;s a simple concrete example of an abstract idea &amp;ndash; higher-order functions, or decorators, or basic metaprogramming. It&amp;rsquo;s &lt;em>pedantically&lt;/em> useful, even if it&amp;rsquo;s not &lt;em>practically&lt;/em> useful.&lt;/p>
&lt;p>So now let&amp;rsquo;s talk about macros.&lt;/p>
&lt;p>We could use macros to implement something like Python&amp;rsquo;s decorators &amp;ndash; a better &lt;em>notation&lt;/em> for doing something that we can already do without them. And this is great! Within limits, this is great. But that&amp;rsquo;s not what this blog post is about.&lt;/p>
&lt;p>Instead, I want to talk about something that you can&amp;rsquo;t really do without macros. A new expressive power that macros enable, something more than just notational freedom.&lt;/p>
&lt;p>So bear with me for a moment, because I know this is a little silly. But let&amp;rsquo;s consider the following alternative memoization scheme:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defn dumb-example [x]
(while (some-complicated-stuff-happens)
(pretend-like-this-function-is-big))
(def result (memoize (do-something-very-expensive x)))
(do-more-interesting-work))
&lt;/code>&lt;/pre>&lt;p>Instead of memoizing a function to return a new memoized function, I want to memoize an &lt;em>expression&lt;/em>. The &lt;code>memoized&lt;/code> examples we saw earlier took place on the &lt;em>callee&lt;/em> side &amp;ndash; the function declaration &amp;ndash; but here I want the memoization to happen on the &lt;em>caller&lt;/em> side &amp;ndash; at the function invocation.&lt;/p>
&lt;p>I want it to be the case that this function only &lt;em>actually&lt;/em> calls &lt;code>do-something-very-expensive&lt;/code> once per unique value of &lt;code>x&lt;/code>, even across separate invocations of &lt;code>dumb-example&lt;/code>. And if you can stop wondering &lt;em>why&lt;/em> you&amp;rsquo;d want to do this, think for a moment about &lt;em>how&lt;/em> you would do this. How could you even write this in JavaScript?&lt;/p>
&lt;p>We&amp;rsquo;ll simplify it a little: instead of worrying about arbitrary expressions, let&amp;rsquo;s assume that this is always a function call with a single argument, like we did before:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="kr">const&lt;/span> &lt;span class="nx">dumbExample&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">x&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">while&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">someComplicatedStuffHappens&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">pretendLikeThisFunctionIsBig&lt;/span>&lt;span class="p">();&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">memoize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">doSomethingVeryExpensive&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">x&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">doMoreInterestingWork&lt;/span>&lt;span class="p">();&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>How do you implement &lt;code>memoize&lt;/code>?&lt;/p>
&lt;p>I &lt;em>think&lt;/em> that you basically can&amp;rsquo;t, in JavaScript. Or, more accurately: I can&amp;rsquo;t think of a way to do it.&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;/p>
&lt;p>You can easily implement something very &lt;em>similar&lt;/em>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="kr">const&lt;/span> &lt;span class="nx">resultMap&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Map&lt;/span>&lt;span class="p">();&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">dumbExample&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">x&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">while&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">someComplicatedStuffHappens&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">pretendLikeThisFunctionIsBig&lt;/span>&lt;span class="p">();&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">memoize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">resultMap&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">doSomethingVeryExpensive&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">x&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">doMoreInterestingWork&lt;/span>&lt;span class="p">();&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you can manually find a place to hoist the memoization dictionary. And, you know, in practice this would be completely fine. But it wouldn&amp;rsquo;t teach us anything new.&lt;/p>
&lt;p>The problem is that we need some place to store the results that&amp;rsquo;s somehow tied to this &lt;em>callsite&lt;/em>. Not the function, not the program containing the function, but this particular invocation of &lt;code>memoize&lt;/code>.&lt;/p>
&lt;p>And macros let us do exactly that: a macro will let us allocate, at &lt;em>compile time&lt;/em>, a dictionary of results which we can then reference at &lt;em>runtime&lt;/em>.&lt;/p>
&lt;p>Which would have been a very surprising statement to me, a few years ago. Before I started writing Janet, &lt;a href="https://ianthehenry.com/posts/janet-game/the-problem-with-macros/">I thought that macros were &lt;em>syntactic&lt;/em> transformations&lt;/a>. And in some languages &amp;ndash; Rust or OCaml, say &amp;ndash; that&amp;rsquo;s exactly what they are. Syntactic transformations, AST twisters, token shufflers. Still very useful! But you can&amp;rsquo;t write a purely syntactic macro that admits per-callsite memoization.&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>&lt;/p>
&lt;p>But in &lt;a href="https://janet-lang.org/">Janet&lt;/a>, and some other lisp-family languages, macros are so much more than syntactic transformations. They&amp;rsquo;re ways to execute code &amp;ndash; any code &amp;ndash; and dynamically generate new functions &amp;ndash; any functions &amp;ndash; which will become our eventual program. Let&amp;rsquo;s take a look:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defmacro memoize [[f &amp;amp; args]]
(def results @{})
~(let [f ,f
args [,;args]
memoization-key [f args]]
(if (has-key? ,results memoization-key)
(in ,results memoization-key)
(let [result (f ;args)]
(put ,results memoization-key result)
result))))
(memoize (+ 1 2))
&lt;/code>&lt;/pre>&lt;aside>
&lt;p>In Janet, quasiquote is spelled &lt;code>~&lt;/code>, and unquote-splice is spelled &lt;code>,;&lt;/code>. The semicolon is pronounced &amp;ldquo;splice,&amp;rdquo; and &lt;code>;args&lt;/code> is like &lt;code>...args&lt;/code> in JavaScript. &lt;code>@{}&lt;/code> is how you construct an empty &amp;ldquo;table,&amp;rdquo; which is Janet&amp;rsquo;s word for a mutable hashtable.&lt;/p>
&lt;/aside>
&lt;p>Set aside the horrific hygiene crimes in this macro definition, and just think about &lt;code>results&lt;/code>. We allocated that &lt;em>at compile time&lt;/em>, during macro expansion. Just one table! And then we &lt;code>unquote&lt;/code>d multiple references to it into our macro expansion.&lt;/p>
&lt;p>And this &lt;em>absolutely does not work&lt;/em>.&lt;/p>
&lt;p>It doesn&amp;rsquo;t work because &lt;code>unquote&lt;/code> puts this table into the abstract syntax tree of our function, and tables, when they&amp;rsquo;re interpreted as abstract syntax trees and evaluated by Janet, create new tables. The empty table &lt;code>@{}&lt;/code> is just how Janet represents the abstract syntax tree for an empty table literal.&lt;/p>
&lt;p>So if we look at the macro expansion for our call:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(let [f +
args [1 2]
memoization-key [f args]]
(if (has-key? @{} memoization-key)
(in @{} memoization-key)
(let [result (f (splice args))]
(put @{} memoization-key result)
result)))
&lt;/code>&lt;/pre>&lt;p>We&amp;rsquo;re allocating a brand new table every time we reference it! We didn&amp;rsquo;t put &amp;ldquo;a reference to the table we allocated at compile time&amp;rdquo; into the function. We put &amp;ldquo;the abstract syntax tree of a table literal&amp;rdquo; into our function. It doesn&amp;rsquo;t matter that every one of those tables in the abstract syntax tree is actually the &lt;em>same&lt;/em> table. When Janet&amp;rsquo;s &lt;code>compile&lt;/code> function sees a table &amp;ndash; any table &amp;ndash; it interprets that as &amp;ldquo;allocate a new table.&amp;rdquo;&lt;/p>
&lt;p>So we need some way to say &amp;ldquo;no no no, this value isn&amp;rsquo;t an abstract syntax tree; it&amp;rsquo;s just a value. I just want a literal reference to this exact table that I allocated at compile time; I don&amp;rsquo;t want you to interpret it as an abstract syntax tree at all.&amp;rdquo;&lt;/p>
&lt;p>And you can do that, by quoting the table:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defmacro memoize [[f &amp;amp; args]]
(def results @{})
~(let [f ,f
args [,;args]
memoization-key [f args]]
(if (has-key? ',results memoization-key)
(in ',results memoization-key)
(let [result (f ;args)]
(put ',results memoization-key result)
result))))
(memoize (+ 1 2))
&lt;/code>&lt;/pre>&lt;p>We still unquote &lt;code>results&lt;/code>, but we unquote it inside a &lt;code>quote&lt;/code> form. We quote-unquote the table.&lt;/p>
&lt;p>And this works! Every time this macro invocation appears, it will expand to an abstract syntax tree like this:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(let [f +
args [1 2]
memoization-key [f args]]
(if (has-key? (quote @{}) memoization-key)
(in (quote @{}) memoization-key)
(let [result (f (splice args))]
(put (quote @{}) memoization-key result)
result)))
&lt;/code>&lt;/pre>&lt;p>And, well, you can&amp;rsquo;t see this when we print it out like this, but every one of those tables is still the same table. Maybe this is an easier way to visualize what&amp;rsquo;s happening:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(let [f +
args [1 2]
memoization-key [f args]]
(if (has-key? (quote &amp;lt;pointer-to-our-one-table&amp;gt;) memoization-key)
(in (quote &amp;lt;pointer-to-our-one-table&amp;gt;) memoization-key)
(let [result (f (splice args))]
(put (quote &amp;lt;pointer-to-our-one-table&amp;gt;) memoization-key result)
result)))
&lt;/code>&lt;/pre>&lt;p>And any time Janet evaluates &lt;code>(quote _)&lt;/code>, it doesn&amp;rsquo;t look at the thing being quoted. It just returns it. Even if it&amp;rsquo;s a reference-style, mutable value like it is here.&lt;/p>
&lt;aside>
&lt;p>But wait. If Janet doesn&amp;rsquo;t look at what&amp;rsquo;s inside &lt;code>(quote _)&lt;/code>, how does the table get there in the first place? If we write &lt;code>',results&lt;/code> &amp;ndash; which is syntax sugar for &lt;code>(quote (unquote results))&lt;/code>, wouldn&amp;rsquo;t we expect that to give us the list &lt;code>['unquote 'results]&lt;/code>?&lt;/p>
&lt;p>Well, even though &lt;code>quote&lt;/code> is special to Janet&amp;rsquo;s &lt;code>compile&lt;/code> function, it&amp;rsquo;s not special to anyone else. And when &lt;code>quasiquote&lt;/code> runs and resolves all of the &lt;code>unquote&lt;/code>s, it doesn&amp;rsquo;t treat &lt;code>quote&lt;/code> differently than any other form. So by the time Janet goes to compile &lt;code>',results&lt;/code>, the &lt;code>unquote&lt;/code> has already disappeared, and it only sees &lt;code>(quote &amp;lt;pointer-to-our-one-table&amp;gt;)&lt;/code>.&lt;/p>
&lt;/aside>
&lt;p>This was a very long way to point out a pretty simple thing, but I wanted to write this post because &lt;em>I wish that I&amp;rsquo;d understood this sooner&lt;/em>. It took a long time before &lt;code>quote&lt;/code> &amp;ldquo;clicked&amp;rdquo; for me. I was &lt;em>using&lt;/em> &lt;code>quote&lt;/code> to write macros long before I actually understood how it worked, but I was not using it to its fullest.&lt;/p>
&lt;p>If you&amp;rsquo;d asked me two years ago, I probably would have said that &lt;code>quote&lt;/code> &amp;ldquo;returns the abstract syntax tree representing its argument.&amp;rdquo; &lt;code>(+ 1 2)&lt;/code> is &lt;code>3&lt;/code>, and &lt;code>'(+ 1 2)&lt;/code> is the abstract syntax tree &lt;code>['+ 1 2]&lt;/code> that represents that function call.&lt;/p>
&lt;p>And while this seems &lt;em>kinda&lt;/em> right, it&amp;rsquo;s just not. &lt;code>quote&lt;/code> doesn&amp;rsquo;t &amp;ldquo;return the abstract syntax tree&amp;rdquo; of anything. &lt;code>quote&lt;/code> returns &lt;em>whatever you hand it&lt;/em>. The abstract syntax tree &lt;code>['+ 1 2]&lt;/code> came from the Janet &lt;em>parser&lt;/em>. The parser created that; &lt;code>quote&lt;/code> had nothing to do with it. &lt;code>quote&lt;/code> just forwarded it along.&lt;/p>
&lt;p>Even the fact that I&amp;rsquo;m using &lt;code>'+&lt;/code> to mean &amp;ldquo;the symbol that is just the plus sign&amp;rdquo; is a little funny. Maybe it&amp;rsquo;s easier to think of this as the abstract syntax tree &lt;code>[(symbol &amp;quot;+&amp;quot;) 1 2]&lt;/code>. The symbol, once again, came from the parser, not the &lt;code>quote&lt;/code>. But I&amp;rsquo;m just so used to thinking of &lt;code>'+&lt;/code> as &amp;ldquo;the way you write a symbol literal.&amp;rdquo;&lt;/p>
&lt;p>Anyway, we can observe this property of &lt;code>quote&lt;/code> in a much simpler &amp;ndash; albeit stranger &amp;ndash; scenario. What does this function do?&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defn foo []
(def x @{})
(put x (length x) true)
x)
&lt;/code>&lt;/pre>&lt;p>It&amp;rsquo;s not a trick question; it does what you&amp;rsquo;d expect:&lt;/p>
&lt;pre tabindex="0">&lt;code>repl:1:&amp;gt; (foo)
@{0 true}
repl:2:&amp;gt; (foo)
@{0 true}
repl:2:&amp;gt; (foo)
@{0 true}
&lt;/code>&lt;/pre>&lt;p>But what about &lt;em>this&lt;/em> one?&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defn foo []
(def x '@{})
(put x (length x) true)
x)
&lt;/code>&lt;/pre>&lt;p>It&amp;rsquo;s&amp;hellip; well, it&amp;rsquo;s not a trick question either, but this definitely would have confused me earlier in my Janet career:&lt;/p>
&lt;pre tabindex="0">&lt;code>repl:1:&amp;gt; (foo)
@{0 true}
repl:2:&amp;gt; (foo)
@{0 true 1 true}
repl:3:&amp;gt; (foo)
@{0 true 1 true 2 true}
repl:4:&amp;gt; (foo)
@{0 true 1 true 2 true 3 true}
&lt;/code>&lt;/pre>&lt;p>It&amp;rsquo;s the same table every time, because there was only ever one table in the first place: the table that the Janet &lt;em>parser&lt;/em> allocated to represent the abstract syntax tree of the characters &lt;code>@{}&lt;/code>.&lt;/p>
&lt;p>And we&amp;rsquo;re mutating it.&lt;/p>
&lt;p>Neat.&lt;/p>
&lt;p>Is this useful? Well&amp;hellip; it&amp;rsquo;s sort of like a &lt;code>static&lt;/code> storage qualifier&amp;hellip;? But&amp;hellip; but no. It&amp;rsquo;s not really useful. I struggle to even think of a situation where you would want a per-callsite &lt;code>(memoize)&lt;/code>, and in any case it would be &lt;em>easier&lt;/em> to write that particular macro without using quote-unquote at all:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defmacro memoize [form]
(def results @{})
(defn memoize-call [f &amp;amp; args]
(def memoization-key [f args])
(if (has-key? results memoization-key)
(in results memoization-key)
(let [result (f ;args)]
(put results memoization-key result)
result)))
~(,memoize-call ,;form))
&lt;/code>&lt;/pre>&lt;p>In this version we allocate a single closure at compile time, and insert a reference to &lt;em>that&lt;/em> into the expanded form. The closure refers to the shared table, and we no longer have to think about gensym, or argument evaluation order, or quote-unquoting, or anything else. We&amp;rsquo;re back in the safe land of writing normal code with values and closures, instead of the weird upside-down world of writing quasiquoted code with abstract syntax trees and evaluation rules.&lt;/p>
&lt;p>&lt;em>But&lt;/em>. Using quote-unquote to pass values from compile time to runtime is still a &lt;em>generally&lt;/em> useful technique. You can&amp;rsquo;t always write a closure like this &amp;ndash; sometimes you really do need to produce an AST with a reference to a mutable value you allocated at compile time.&lt;/p>
&lt;p>The problem is, it&amp;rsquo;s useful in situations that don&amp;rsquo;t really fit into a blog post. I&amp;rsquo;ve been spending some time recently rewriting the &lt;a href="https://bauble.studio/">Bauble&lt;/a> compiler, now that I actually know Janet, and this quote-unquote pattern comes up in just about every nontrivial macro I write. These are macros that produce Janet code that you can evaluate to construct a GLSL AST, but some amount of evaluation happens at macro expansion time, and the result of that ahead-of-time evaluation gets quote-unquoted into the intermediate Janet code.&lt;/p>
&lt;p>I&amp;rsquo;ve also used quote-unquote in conjunction with mutable values allocated at compile-time (like what we saw here) in &lt;a href="https://ianthehenry.com/posts/my-kind-of-repl/">Judge&lt;/a>, my inline snapshot testing framework, to ensure that every assertion you &lt;em>write&lt;/em> actually runs by the time a test completes.&lt;/p>
&lt;p>But, well, those are big and complicated and hard to explain and talk about, and I just wanted to point out how I underestimated &lt;code>quote&lt;/code> for so long, in case you&amp;rsquo;ve been balancing your parentheses under the same misconception.&lt;/p>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>You could create a global memoization map keyed on the &lt;em>function&lt;/em> that you&amp;rsquo;re calling, but this would actually have different semantics than I&amp;rsquo;m imagining. If I said &lt;code>memoize(f, 1) + memoize(f, 1)&lt;/code> I would expect those to each invoke &lt;code>f&lt;/code>, because instances of &lt;code>memoize&lt;/code> shouldn&amp;rsquo;t share results. Why not? Because this is a fake example, and a global memoization is a different (easier!) thing than per-call-site memoization.&lt;/p>
&lt;p>Update: &lt;a href="https://news.ycombinator.com/item?id=41244532">someone on Hacker News pointed out&lt;/a> that you actually &lt;em>can&lt;/em> do this if you write it as a template application instead of a normal function call, because applications of constant templates pass a unique reference per &lt;em>callsite&lt;/em> &amp;ndash; even when the template function itself varies. Amusingly, the intended purpose of this feature is memoization of template expansions&amp;hellip;&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2" role="doc-endnote">
&lt;p>If macros could rewrite the syntax &lt;em>around&lt;/em> themselves, instead of just rewriting themselves, then you could imagine hoisting a &lt;code>const resultMap = new Map();&lt;/code> &lt;em>outside&lt;/em> of the function definition and doing this as a purely syntactic transformation. But as far as I know, no one has actually implemented &lt;a href="https://ianthehenry.com/posts/generalized-macros/">a macro system powerful enough to do that&lt;/a>.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><pubDate>Mon, 12 Aug 2024 00:00:00 +0000</pubDate><link>https://ianthehenry.com/posts/quote-unquote-macros/</link><guid isPermaLink="true">https://ianthehenry.com/posts/quote-unquote-macros/</guid></item><item><title>How to Learn Nix, Part 49: nix-direnv is a huge quality of life improvement</title><description>&lt;p>The &lt;em>reason&lt;/em> I &lt;a href="https://ianthehenry.com/posts/how-to-learn-nix/installing-nix-on-macos/">discovered an ancient blog post&lt;/a> the other day was that I had something new to say about Nix for the first time in over two years.&lt;/p>
&lt;p>The thing I want to say is this: &lt;a href="https://github.com/nix-community/nix-direnv">&lt;code>nix-direnv&lt;/code>&lt;/a> is great. It fixes roughly every problem that I&amp;rsquo;ve had with &lt;code>nix-shell&lt;/code>, and does so in a much nicer way than my previous ad-hoc solutions.&lt;/p>
&lt;p>This is important because I &lt;em>mostly&lt;/em> just use Nix to document and install per-project native dependencies. I do use it to install &amp;ldquo;global&amp;rdquo; tools as well, but that is &lt;a href="https://ianthehenry.com/posts/janet-game/how-to-patch-emacs/">rarely very interesting&lt;/a>, and most of my interaction with Nix these days consists of editing small &lt;code>shell.nix&lt;/code> files.&lt;/p>
&lt;p>But it took a bit of doing to get to the point that I felt &lt;em>good&lt;/em> about using Nix for this. For one thing, shells don&amp;rsquo;t register GC roots, which means that every time you collect garbage you have to re-download all the dependencies for the project you were working on. We overcame that hurdle in &lt;a href="https://ianthehenry.com/posts/how-to-learn-nix/saving-your-shell/">part 37&lt;/a>, by making a custom wrapper around &lt;code>nix-shell&lt;/code> that sets up GC roots correctly, but it was surprisingly difficult.&lt;/p>
&lt;p>For another thing, Nix is pretty insistent that you use &lt;em>bash&lt;/em> as your interactive shell. I figured out a workaround for that in &lt;a href="https://ianthehenry.com/posts/how-to-learn-nix/nix-zshell/">Nix classic&lt;/a>, but &lt;a href="https://ianthehenry.com/posts/how-to-learn-nix/nix-develop/">essentially failed&lt;/a> to make &lt;code>nix develop&lt;/code> similarly usable.&lt;/p>
&lt;p>&lt;a href="https://github.com/nix-community/nix-direnv">&lt;code>nix-direnv&lt;/code>&lt;/a> solves both of these problems. Instead of spawning a new shell, it just adds environment variables to your existing shell. And when it evaluates &lt;code>shell.nix&lt;/code>, it automatically registers the result as a GC root.&lt;/p>
&lt;p>It also only re-evaluates &lt;code>shell.nix&lt;/code> when it actually changes, which means that it in the typical case there&amp;rsquo;s no startup time. In contrast, my GC-root-installing wrapper takes about 750ms to open a typical shell (raw &lt;code>nix-shell&lt;/code>, without the GC root evaluation dance, takes only 400ms). This doesn&amp;rsquo;t sound very long, because it&amp;rsquo;s not &amp;ndash; I&amp;rsquo;m running Nix on what I can only characterize as a supercomputer. But I originally installed Nix on a laptop that pre-dated germ theory, and its startup latency was a lot more annoying.&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;/p>
&lt;p>&lt;code>nix-direnv&lt;/code> also automatically updates the environment when &lt;code>shell.nix&lt;/code> changes, so you don&amp;rsquo;t have to close and re-open your &lt;code>nix-shell&lt;/code> whenever you add a dependency. Not only is this ergonomically better, but it also means that you don&amp;rsquo;t mess up your shell history every time you add a dependency or exit a project.&lt;/p>
&lt;p>I had never used &lt;a href="https://direnv.net/">&lt;code>direnv&lt;/code>&lt;/a> before, and to this date the only thing I&amp;rsquo;ve used it for is managing my Nix shells. But it&amp;rsquo;s a general tool for managing per-directory environment variables, which is &lt;em>essentially&lt;/em> all that &lt;code>nix-shell&lt;/code> is. &lt;code>nix-shell&lt;/code> can also register bash functions &amp;ndash; if you&amp;rsquo;re using bash &amp;ndash; which is useful if you want to use it to debug a derivation. But for my purposes, environment variables are all I really need.&lt;/p>
&lt;p>&lt;code>direnv&lt;/code> has some built-in support for Nix, but it isn&amp;rsquo;t great; &lt;a href="https://github.com/direnv/direnv/wiki/Nix#some-factors-to-consider">direnv publishes a table outlining some of the advantages&lt;/a> of using &lt;code>nix-direnv&lt;/code>. &lt;code>nix-direnv&lt;/code> is some sort of plugin(?) that replaces the native Nix support with something much better. And it&amp;rsquo;s great. It makes the &amp;ldquo;reproducible developer environment&amp;rdquo; aspect of Nix just work™. And it&amp;rsquo;s pretty easy to use:&lt;/p>
&lt;p>First off, install &lt;code>nixpkgs.direnv&lt;/code> and &lt;code>nixpkgs.nix-direnv&lt;/code>.&lt;/p>
&lt;p>I installed them with &lt;code>nix-env&lt;/code>, using the same declarative wrapper that I wrote in &lt;a href="https://ianthehenry.com/posts/how-to-learn-nix/declarative-user-environment/">part 22&lt;/a>. If you install &lt;code>nix-direnv&lt;/code> in a different way, the following will be different.&lt;/p>
&lt;p>Installing &lt;code>nix-direnv&lt;/code> doesn&amp;rsquo;t &amp;ldquo;enable&amp;rdquo; the plugin; you have to separately tell &lt;code>direnv&lt;/code> about it:&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">mkdir -p ~/.config/direnv
&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;source ~/.nix-profile/share/nix-direnv/direnvrc&amp;#39;&lt;/span> &amp;gt; ~/.config/direnv/direnvrc
&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;eval &amp;#34;$(direnv hook zsh)&amp;#34;&amp;#39;&lt;/span> &amp;gt;&amp;gt; ~/.zshrc
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Once you do that, you have to run the following commands in every directory that you want to nix-shellify:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;use nix&amp;#39;&lt;/span> &amp;gt; .envrc
direnv allow
&lt;/code>&lt;/pre>&lt;/div>&lt;p>And you&amp;rsquo;re done. That&amp;rsquo;s it! Now every time you navigate to that directory, you&amp;rsquo;ll have&amp;hellip;&lt;/p>
&lt;pre class="terminal">&lt;code>$ cd ~src/project
direnv: loading ~/src/project/.envrc
direnv: using nix
direnv: nix-direnv: using cached dev shell
direnv: export +CONFIG_SHELL +HOST_PATH +IN_NIX_SHELL +MACOSX_DEPLOYMENT_TARGET +NIX_BUILD_CORES +NIX_CFLAGS_COMPILE +NIX_COREFOUNDATION_RPATH +NIX_DONT_SET_RPATH +NIX_DONT_SET_RPATH_FOR_BUILD +NIX_ENFORCE_NO_NATIVE +NIX_IGNORE_LD_THROUGH_GCC +NIX_INDENT_MAKE +NIX_NO_SELF_RPATH +NIX_STORE +PATH_LOCALE +SOURCE_DATE_EPOCH +XDG_DATA_DIRS +__darwinAllowLocalNetworking +__impureHostDeps +__propagatedImpureHostDeps +__propagatedSandboxProfile +__sandboxProfile +buildInputs +builder +configureFlags +depsBuildBuild +depsBuildBuildPropagated +depsBuildTarget +depsBuildTargetPropagated +depsHostHost +depsHostHostPropagated +depsTargetTarget +depsTargetTargetPropagated +doCheck +doInstallCheck +dontAddDisableDepTrack +gl_cv_func_getcwd_abort_bug +name +nativeBuildInputs +nobuildPhase +out +outputs +patches +phases +propagatedBuildInputs +propagatedNativeBuildInputs +shell +shellHook +stdenv +strictDeps +system -PS2 ~PATH&lt;/code>&lt;/pre>
&lt;p>Oh. Well that&amp;rsquo;s not great.&lt;/p>
&lt;p>By default &lt;code>direnv&lt;/code> prints every environment variable that it adds, removes, or changes. Which makes sense if you&amp;rsquo;re using it for, like, credentials or something, but for Nix shells it&amp;rsquo;s just a waste of scrollback.&lt;/p>
&lt;p>There&amp;rsquo;s not really a simple way to suppress printing that giant &lt;code>export&lt;/code> line, but you can hack it away by adding something like this to your &lt;code>.zshrc&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="nb">export&lt;/span> &lt;span class="nv">DIRENV_LOG_FORMAT&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">printf&lt;/span> &lt;span class="s2">&amp;#34;\033[2mdirenv: %%s\033[0m&amp;#34;&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;span class="nb">eval&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>direnv hook zsh&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
_direnv_hook&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;span class="nb">eval&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>direnv &lt;span class="nb">export&lt;/span> zsh 2&amp;gt; &amp;gt;&lt;span class="o">(&lt;/span>egrep -v -e &lt;span class="s1">&amp;#39;^....direnv: export&amp;#39;&lt;/span> &amp;gt;&lt;span class="p">&amp;amp;&lt;/span>2&lt;span class="k">)&lt;/span>&lt;span class="s2">)&amp;#34;&lt;/span>
&lt;span class="o">}&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>(The &lt;code>.&lt;/code>s in the regex exclude the &amp;ldquo;dim text&amp;rdquo; control characters at the beginning of the line.)&lt;/p>
&lt;p>That removes the giant export line without removing the rest of the input. And now:&lt;/p>
&lt;pre class="terminal">&lt;code>$ cd ~src/project
direnv: loading ~/src/project/.envrc
direnv: using nix
direnv: nix-direnv: using cached dev shell&lt;/code>&lt;/pre>
&lt;p>Ahhh. That&amp;rsquo;s better.&lt;/p>
&lt;p>I&amp;rsquo;ve been using &lt;code>nix-direnv&lt;/code> for a few months now, and I must say: I wish that I had installed it sooner. It&amp;rsquo;s a &lt;em>much&lt;/em> nicer experience than the default &lt;code>nix-shell&lt;/code>, and I&amp;rsquo;m happy that I can get rid of the bespoke hacks that I&amp;rsquo;ve accrued over the years.&lt;/p>
&lt;p>&amp;hellip;almost. The one thing this does not help with is &lt;code>nix-shell -p&lt;/code>. &lt;code>nix-shell -p&lt;/code> is a useful way to &amp;ldquo;temporarily&amp;rdquo; install packages without actually putting them on your PATH, and I still use my zsh hack so that &lt;code>nix-shell -p&lt;/code> doesn&amp;rsquo;t drop me into a bash session. Although I do this rarely enough that I could probably just suffer through it.&lt;/p>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>To be fair I use tmux and just always have sessions open for the projects I&amp;rsquo;m working on, so it&amp;rsquo;s not like it was annoying very often.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2" role="doc-endnote">
&lt;p>I think this is the sort of thing that &lt;a href="https://github.com/nix-community/home-manager">&lt;code>home-manager&lt;/code>&lt;/a> does for you automatically, but I don&amp;rsquo;t use &lt;code>home-manager&lt;/code>.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><pubDate>Sun, 28 Jan 2024 00:00:00 +0000</pubDate><link>https://ianthehenry.com/posts/how-to-learn-nix/nix-direnv/</link><guid isPermaLink="true">https://ianthehenry.com/posts/how-to-learn-nix/nix-direnv/</guid></item><item><title>How to Learn Nix, Part 48: Installing (single-user) Nix on macOS</title><description>&lt;blockquote>
&lt;p>Note: I wrote the vast majority of this post on August 24, 2022, but I decided that it was a whiny rant that I didn&amp;rsquo;t want to publish. I came across the draft in January 2024, remembered that this &lt;em>entire series&lt;/em> is a whiny rant, and decided to publish it anyway.&lt;/p>
&lt;/blockquote>
&lt;p>I&amp;rsquo;ve been a happy Nix user for about 18 months now, and&amp;ndash; well, not &lt;em>happy&lt;/em> happy, but satisfied&amp;hellip; no&amp;hellip; not really satisfied either; perhaps it&amp;rsquo;s more of a resigned disgruntlement; a feeling that despite its many flaws, it&amp;rsquo;s still better than anything else out there, and I&amp;rsquo;ve invested so much time into it already that it would be a shame to give up now, so&amp;hellip; am I describing Stockholm syndrome?&lt;/p>
&lt;p>I&amp;rsquo;ve been imprisoned in Nix&amp;rsquo;s castle for about 18 months now, and I recently came into a new laptop &amp;ndash; one of those shiny blue numbers &amp;ndash; so naturally one of the first things I did was install Nix on it.&lt;/p>
&lt;p>It would be nice if this blog post were only like a paragraph long, wouldn&amp;rsquo;t it? Step one: run the installer; there is no step two.&lt;/p>
&lt;p>But I am afraid to report: there is very much a step two.&lt;/p>
&lt;h1 id="a-tale-of-two-nixen">a tale of two nixen&lt;/h1>
&lt;p>There are two flavors of Nix, and you must choose at installation time which variety you will run.&lt;/p>
&lt;p>You can install &amp;ldquo;single-user&amp;rdquo; Nix, in which Nix is a binary that you run as an interactive command that does things and forks subprocesses and works like any other package manager you&amp;rsquo;ve ever used. Or you can install &amp;ldquo;multi-user&amp;rdquo; Nix, in which Nix is a daemon, running as root, and the &lt;code>nix&lt;/code> command merely makes requests to that daemon, which in turn coordinates a set of special &lt;code>nixbld&lt;/code> users that exist so that you don&amp;rsquo;t build all of your software as root.&lt;/p>
&lt;p>If you&amp;rsquo;re using NixOS, you have to use multi-user Nix, which makes sense to me: Nix controls the entire operating system, and there needs to be some central puppeteer to coordinate changes to the kernel or whatever.&lt;/p>
&lt;p>But if you don&amp;rsquo;t use NixOS, then you have a choice. You can choose to set up a persistent daemon, create a dozen &lt;code>nixbld&lt;/code> users, and have every command you run proxy through this centralized service. Or you can choose&amp;hellip; not to do that.&lt;/p>
&lt;p>I don&amp;rsquo;t know why you&amp;rsquo;d choose a multi-user installation when the single-user option is available. There might be a very good reason to prefer multi-user Nix, even when you are installing Nix on a laptop with a single user, but I do not know what it is. Obviously there is no documentation to explain the distinction or persuade the user one way or the other, so I can only speculate.&lt;/p>
&lt;p>And needless to say, when I first tasted the forbidden fruit of the Nix tree all those year ago, I chose a single-user install, and I have never regretted that.&lt;/p>
&lt;p>But then something upsetting happened.&lt;/p>
&lt;p>macOS has always been an unloved corner of Nix, and installing Nix on macOS has been rather fraught with peril ever since macOS Catalina came out.&lt;/p>
&lt;p>The problem seems so trivial: Nix requires a &lt;code>/nix&lt;/code> directory to exist, but macOS does not let you create new top-level root directories, so you have to actually create a separate volume and mount it as &lt;code>/nix&lt;/code>. Which might not sound difficult, but getting that to work with FileVault and the various hardware encryption configurations of different generations of Apple hardware and&amp;hellip; I dunno, probably some other complicating factors, I don&amp;rsquo;t know how computers work&amp;hellip; there was a heroic effort to keep macOS Nix working at all, at a time when some Nix maintainers were arguing that it would be easier to deprecate support for macOS entirely.&lt;/p>
&lt;p>But there was an unfortunate casualty of this struggle: the single-user installation option was removed.&lt;/p>
&lt;p>If you want to install Nix on macOS today, you must use a multi-user installation. That means running &lt;code>nix-daemon&lt;/code>, that means messing with &lt;code>launchctl&lt;/code>, creating a bunch of build users&amp;hellip;&lt;/p>
&lt;p>Which brings things back to me.&lt;/p>
&lt;p>I had a new laptop. I wanted to install Nix on it. And the only option facing me&amp;hellip;&lt;/p>
&lt;p>Was to get in there and hack up an unofficial, unsupported single-user install, because I just can&amp;rsquo;t abide the idea of a multi-user Nix install.&lt;/p>
&lt;p>And it&amp;rsquo;s not &lt;em>only&lt;/em> a philosophical objection! Although I admit that I have a visceral aversion to the idea of installing a daemon running as root for something that &lt;em>absolutely does not need to be a daemon or running as root&lt;/em>, I can understand that many people do not care about that.&lt;/p>
&lt;p>But there is a real, practical reason to avoid the multi-user configuration: if you are using a multi-user Nix install on macOS, then every time you &lt;em>upgrade&lt;/em> macOS, Nix will break, and you will have to &lt;a href="https://github.com/NixOS/nix/issues/3616">go in and do some surgery to fix it&lt;/a>.&lt;/p>
&lt;blockquote>
&lt;p>This was true when I wrote this, in 2022. I don&amp;rsquo;t know if this is still true, but &lt;a href="https://github.com/DeterminateSystems/nix-installer">an alternate Nix installer&lt;/a> &amp;ndash; which also only supports multi-user installs on macOS &amp;ndash; puts &amp;ldquo;survives macOS upgrades&amp;rdquo; at the top of its &lt;a href="https://github.com/DeterminateSystems/nix-installer#motivations">advantages over the official installer&lt;/a>. Which implies to me that this is still a problem.&lt;/p>
&lt;/blockquote>
&lt;p>This is because the multi-user installation edits some of the system-provided config files &amp;ndash; like &lt;code>/etc/bashrc&lt;/code> &amp;ndash; to insert some Nix setup things. And every time (every time?) you upgrade macOS, it will overwrite these files and restore them to the defaults. Is it hard to fix? No. Is it annoying? Yes. Is it sufficiently annoying to try to figure out a single-user install? To each their own.&lt;/p>
&lt;p>In any case: the only difficult thing about installing Nix on macOS is creating that darn &lt;code>/nix&lt;/code> directory. And clearly the multi-user install step knows how to do that. So I should be able to steal that part of it, create the directory, and then manually perform a regular single-user install. Right?&lt;/p>
&lt;p>Right.&lt;/p>
&lt;h1 id="right">right&lt;/h1>
&lt;p>Well it worked great, actually! It wasn&amp;rsquo;t hard at all. The steps are:&lt;/p>
&lt;p>First, download the Nix install script.&lt;/p>
&lt;p>The official Nix installation instructions are of the &lt;code>curl | bash&lt;/code> variety. But if you actually look at the script that you&amp;rsquo;re curling, it just turns around and curls &lt;em>another&lt;/em> script&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> and runs that.&lt;/p>
&lt;p>Actually a directory full of them. So the first thing to do is to get &lt;em>that&lt;/em> directory of actual install instructions.&lt;/p>
&lt;p>&lt;a href="https://github.com/NixOS/nix/tree/master/scripts">You can find them on GitHub&lt;/a>, although I just modified the original script to download the installer, extract it, and then quit before doing anything else (er, and also to not delete the extracted artifacts on &lt;code>EXIT&lt;/code>). The really interesting one is &lt;code>create-darwin-volume.sh&lt;/code>, which, as you can probably guess, creates &lt;code>/nix&lt;/code>.&lt;/p>
&lt;p>But! You cannot just invoke this script by itself. This script is invoked by the larger installation script, which sets some environment variables that the &lt;code>create-darwin-volume&lt;/code> expects to be able to read. &lt;em>Specifically&lt;/em> it expects to be able to read&amp;hellip;&lt;/p>
&lt;p>You know what? I don&amp;rsquo;t think you care. Or rather, if you do care, I&amp;rsquo;m sure that you can figure it out from reading the script. It wasn&amp;rsquo;t very hard, and by the time you&amp;rsquo;re reading this the precise interplay between the two scripts probably changed anyway, and you would have to dive into the code to figure out what the modern version requires anyway.&lt;/p>
&lt;p>Once you&amp;rsquo;ve created your &lt;code>/nix&lt;/code>, modify the &lt;code>install&lt;/code> script to remove the lines that force a multi-user install on macOS:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="k">case&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>uname -s&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> in
&lt;span class="s2">&amp;#34;Darwin&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>
&lt;span class="nv">INSTALL_MODE&lt;/span>&lt;span class="o">=&lt;/span>daemon&lt;span class="p">;;&lt;/span>
*&lt;span class="o">)&lt;/span>
&lt;span class="nv">INSTALL_MODE&lt;/span>&lt;span class="o">=&lt;/span>no-daemon&lt;span class="p">;;&lt;/span>
&lt;span class="k">esac&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then run the &lt;code>install&lt;/code> script, and you&amp;rsquo;re done.&lt;/p>
&lt;h1 id="or-just-like-just-make-a-multi-user-install">or just, like, just make a multi-user install&lt;/h1>
&lt;blockquote>
&lt;p>It&amp;rsquo;s the future now &amp;ndash; I wrote this section in 2024, which is part of why this segue is so abrupt.&lt;/p>
&lt;/blockquote>
&lt;p>So, I didn&amp;rsquo;t do that. I am still running the single-user nix install that I hacked together a year and a half ago. I was kind of worried initially that I was overlooking something that made this more complicated and it would break the moment I upgraded Nix, but no: it has held up perfectly. I don&amp;rsquo;t know why the official install script forbids single-user installs on macOS, because they seem to work better than multi-user installs &amp;ndash; I have never once had to think about Nix during a macOS upgrade.&lt;/p>
&lt;p>But I feel that I should correct something that I wrote previously: there are actual reasons why you might &lt;em>prefer&lt;/em> a multi-user install, even on macOS.&lt;/p>
&lt;p>It&amp;rsquo;s actually kind of &lt;em>nice&lt;/em> to run Nix builds as special &lt;code>nixbld&lt;/code> users. Those users &lt;em>only&lt;/em> have permission to read and write to the Nix store; they can&amp;rsquo;t accidentally stick config files in your home directory during install time, for example, or reference a source file that you didn&amp;rsquo;t actually copy to the store. Whereas, with a single-user install, it&amp;rsquo;s possible to write Nix expressions that &amp;ndash; accidentally or not &amp;ndash; depend on files anywhere on your computer.&lt;/p>
&lt;p>So the multi-user install has two advantages:&lt;/p>
&lt;ol>
&lt;li>You get some level of sandboxing/protection from malicious packages&lt;/li>
&lt;li>You get an error when you write a non-hermetic nix expression, instead of silently succeeding&lt;/li>
&lt;/ol>
&lt;p>Those advantage don&amp;rsquo;t seem worth any added complexity to me, let alone having to deal with upgrade problems, but not everyone uses Nix exactly the way that I do.&lt;/p>
&lt;p>(1) is interesting from a security perspective &lt;em>in theory&lt;/em>, but since I usually download pre-built derivations from the binary cache and then run it as myself, I am already vulnerable to malicious nixpkgs. Not &lt;em>as&lt;/em> vulnerable, perhaps, if the binary cache did some kind of fancy virus scanning of uploaded executables&amp;hellip; but we&amp;rsquo;re in the same ballpark.&lt;/p>
&lt;p>I don&amp;rsquo;t really care about (2) at all, although I could see this being useful to people who regularly share Nix expressions with other human people. The cost of making a mistake here just seems so low to me, and the fix is so simple, but I guess if you make this mistake very often it would be nice to hear about it right away.&lt;/p>
&lt;p>There might be even more advantages than these! But we have once again reached the limits of my knowledge.&lt;/p>
&lt;h1 id="why-do-you-care-so-much">why do you care so much&lt;/h1>
&lt;blockquote>
&lt;p>It&amp;rsquo;s 2022 again.&lt;/p>
&lt;/blockquote>
&lt;p>I can&amp;rsquo;t really explain why I&amp;rsquo;m so averse to a multi-user Nix installation. But I am.&lt;/p>
&lt;p>And I know that if I had met Nix just a little bit later in life &amp;ndash; after 2.4 was released, when the single-user install option was deprecated &amp;ndash; I never would have come back to Nix in the first place.&lt;/p>
&lt;p>Which is a shame, because I really &lt;em>do&lt;/em> like Nix, and as much as I like to razz it, I still prefer it &amp;ndash; philosophically and practically &amp;ndash; to Homebrew.&lt;/p>
&lt;p>So I am writing this post to say: you &lt;em>can&lt;/em> still install Nix on macOS like any other package manager. You don&amp;rsquo;t need to take one look at it and go &amp;ldquo;whoa what the heck I&amp;rsquo;m not setting up some root-user daemon just to download packages from the internet&amp;rdquo; and close the tab. You can still use it like you would use any other package manager. You just&amp;hellip; have to get your hands dirty first.&lt;/p>
&lt;p>Which might actually be the perfect introduction to Nix, come to think of it.&lt;/p>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>Amusingly, even though the first step included no checksum (&lt;a href="https://nixos.org/download.html#nix-verify-installation">though you can find one if you care&lt;/a>), this script &lt;em>does&lt;/em> checksum the downloaded file. Maybe because it&amp;rsquo;s served from a different subdomain? I don&amp;rsquo;t know.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><pubDate>Fri, 26 Jan 2024 00:00:00 +0000</pubDate><link>https://ianthehenry.com/posts/how-to-learn-nix/installing-nix-on-macos/</link><guid isPermaLink="true">https://ianthehenry.com/posts/how-to-learn-nix/installing-nix-on-macos/</guid></item><item><title>The Fibonacci Matrix</title><description>&lt;p>When you think about the Fibonacci sequence, you probably imagine a swirling vortex of oscillating points stretching outwards to infinity:&lt;/p>
&lt;p class="canvas-container">&lt;canvas id="teaser" width="384" height="256">&lt;a class="image-container" href="https://ianthehenry.com/posts/fibonacci/hero.f8f0320540a779c19ec927ada02ec9ecbfd90a6d6d418f8e71e99ac4e5d4deca.png">&lt;picture>
&lt;img
class="pixel-art"
src="https://ianthehenry.com/posts/fibonacci/hero.f8f0320540a779c19ec927ada02ec9ecbfd90a6d6d418f8e71e99ac4e5d4deca.png"
width="768"
height="512"
style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAFCAIAAAD38zoCAAAAcklEQVR4nEzNIQoFIRCA4XVnQBDGbrQIZg8wYPI43k7wEl7AYPASIugLr&amp;#43;wf//Lh80kIoZQKITCz&amp;#43;C8AkFI651JKY4xSinjfFxGNMd57ImqtzTnXWqi1jjFaa2utvfe99znn3vvknJmZiADg6/0CAAD//6ojIewsRzhzAAAAAElFTkSuQmCC);"
/>&lt;/a>
&lt;/canvas>&lt;/p>
&lt;p>Okay, no, obviously you don&amp;rsquo;t. &lt;em>Yet&lt;/em>.&lt;/p>
&lt;p>When you think about the Fibonacci sequence, you probably flush with a latent rage when you remember that it is, more often than not, the way that we introduce the concept of &amp;ldquo;recursive functions&amp;rdquo; to new programmers, in some sort of cruel hazing intended to make it harder for them to ever appreciate how recursion can help them write better programs. Sometimes we even add memoization, and call it &amp;ldquo;dynamic programming,&amp;rdquo; in order to impress upon them that even the most trivial problems deserve complex, inefficient solutions.&lt;/p>
&lt;p>Er, okay, you probably don&amp;rsquo;t think about the Fibonacci sequence much at all. It doesn&amp;rsquo;t, you know, come up very often.&lt;/p>
&lt;p>But I hope that you will spend some time thinking about it with me today, because I think that the Fibonacci sequence &amp;ndash; despite being a terrible showcase for recursion &amp;ndash; is a really interesting vector for discussing some techniques from linear algebra.&lt;/p>
&lt;div class="table-container">
&lt;table class="fib-table">
&lt;thead>&lt;tr>&lt;th>how to fibonacci&lt;/th>&lt;th>space complexity&lt;/th>&lt;th>time complexity&lt;/th>&lt;/tr>&lt;/thead>
&lt;tbody>
&lt;tr>&lt;td>insane recursion&lt;/td>&lt;td class="bad">linear&lt;/td>&lt;td class="bad">exponential&lt;/td>&lt;/tr>
&lt;tr>&lt;td>memoized insane recursion&lt;/td>&lt;td class="bad">linear&lt;/td>&lt;td>linear&lt;/td>&lt;/tr>
&lt;tr>&lt;td>trivial iteration&lt;/td>&lt;td class="good">constant&lt;/td>&lt;td>linear&lt;/td>&lt;/tr>
&lt;tr>&lt;td>exponentiation-by-squaring&lt;/td>&lt;td class="good">constant&lt;/td>&lt;td class="good">logarithmic&lt;/td>&lt;/tr>
&lt;tr>&lt;td>eigendecomposition&lt;/td>&lt;td colspan="2">let's talk&lt;/td>&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;p>We will spend no time on the recursive Fibonaccis; I&amp;rsquo;m sure that you&amp;rsquo;ve seen them before. Instead, let&amp;rsquo;s skip right to the &amp;ldquo;obvious&amp;rdquo; way to calculate Fibonacci numbers:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="kd">function&lt;/span> &lt;span class="nx">fib&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">n&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">n&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kd">let&lt;/span> &lt;span class="nx">current&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kd">let&lt;/span> &lt;span class="nx">previous&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kd">let&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">n&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="o">++&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">next&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">current&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">previous&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">previous&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">current&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">current&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">current&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>No recursion, no memoization. We have two pieces of state: the &amp;ldquo;current number&amp;rdquo; and the &amp;ldquo;previous&amp;rdquo; number, and at every step of the iteration we advance both of these to new values.&lt;/p>
&lt;p>But there&amp;rsquo;s something very interesting about this function: the new values for our state are a &lt;em>linear combination&lt;/em> of the old values.&lt;/p>
&lt;pre>&lt;code>current' = current + previous
previous' = current
&lt;/code>&lt;/pre>
&lt;p>Using &lt;code>x'&lt;/code> to mean &amp;ldquo;the next value for &lt;code>x&lt;/code>.&amp;rdquo;&lt;/p>
&lt;p>And you might recognize this as a &amp;ldquo;system of linear equations.&amp;rdquo; I think it&amp;rsquo;s more obvious when we write it like this:&lt;/p>
&lt;pre>&lt;code>current' = 1 * current + 1 * previous
previous' = 1 * current + 0 * previous
&lt;/code>&lt;/pre>
&lt;p>And you might remember that there&amp;rsquo;s another, more cryptic way to write down a system of linear equations:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>current'&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>previous'&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>current&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>previous&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>This is exactly the same thing! This is just another way of writing the equation &amp;ndash; it&amp;rsquo;s just a shorthand notation.&lt;/p>
&lt;p>Here, let&amp;rsquo;s test it out to make sure of that:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1 • 8 + 1 • 5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1 • 8 + 0 • 5&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>13&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>Well that&amp;rsquo;s exactly what we expected &amp;ndash; 13 is the next Fibonacci number in the sequence, and 8 was the previous one.&lt;/p>
&lt;p>We can, of course, repeat this process, by applying the system of linear equations again:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>13&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>21&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>13&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>Or, to put that another way:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>21&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>13&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>And here&amp;rsquo;s why we care: matrix multiplication is associative, so we can actually think of that like this:&lt;/p>
&lt;div class="math">
&lt;div class="open-paren">&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="close-paren"> &lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>21&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>13&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>Or:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>21&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>13&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>In other words: given a system of linear equations to find the &lt;em>next&lt;/em> state of our iteration, we can square the matrix-of-coefficients of the system to find a new system of linear equations that represents &amp;ldquo;two states from now.&amp;rdquo;&lt;/p>
&lt;p>Of course we don&amp;rsquo;t &lt;em>need&lt;/em> matrices to do this. We can compute a formula for &amp;ldquo;two steps&amp;rdquo; of our iteration using term substitution:&lt;/p>
&lt;pre>&lt;code>current' = current + previous
previous' = current
current'' = current' + previous'
previous'' = current'
current'' = (current + previous) + current
previous'' = (current + previous)
current'' = 2 * current + previous
previous'' = current + previous
&lt;/code>&lt;/pre>
&lt;p>Which is a new system of linear equations &amp;ndash; which we can represent as a matrix as well.&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>current''&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>previous''&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>current&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>previous&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>We got the same result, because of course we did: multiplying by this matrix really &lt;em>means&lt;/em> &amp;ldquo;advance to the next state.&amp;rdquo; Multiplying twice means &amp;ldquo;advance to the next state and then advance to the next state after that.&amp;rdquo;&lt;/p>
&lt;p>And we can keep going. What&amp;rsquo;s the state three steps from now?&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>Or, more concisely:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="pow">3&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>If we do this repeatedly, you might notice a familiar pattern start to emerge:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="pow">4&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>2&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="pow">5&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>3&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="pow">6&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>13&lt;/td>
&lt;td>8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>5&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>Which makes sense, doesn&amp;rsquo;t it? Because if we multiply this matrix with the matrix &lt;code>[1 0]&lt;/code> &amp;ndash; our starting values &amp;ndash; then it&amp;rsquo;s going to advance forward through six steps of the Fibonacci sequence in a single leap. So naturally we have to be encoding &lt;em>something&lt;/em> about the sequence itself in the matrix &amp;ndash; otherwise we wouldn&amp;rsquo;t be able to advance by N steps in constant time.&lt;/p>
&lt;p>Now, the insight that takes this from linear to logarithmic is that we don&amp;rsquo;t have to do this multiplication one step at a time. We can multiply in leaps and bounds.&lt;/p>
&lt;p>Let&amp;rsquo;s call our original starting matrix F, for Fibonacci.&lt;/p>
&lt;div class="math">
&lt;div>F&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>We&amp;rsquo;ve already calculated F&lt;sup>2&lt;/sup>:&lt;/p>
&lt;div class="math">
&lt;div>F&lt;sup>2&lt;/sup>&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>And now it&amp;rsquo;s only one more matrix multiplication to calculate F&lt;sup>4&lt;/sup>:&lt;/p>
&lt;div class="math">
&lt;div>F&lt;sup>4&lt;/sup>&lt;/div>
&lt;div>=&lt;/div>
&lt;div>F&lt;sup>2&lt;/sup>F&lt;sup>2&lt;/sup>&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>We can use this fact to calculate arbitrary matrix powers, by breaking the problem up into products of powers of two:&lt;/p>
&lt;div class="inline-math">
&lt;div class="content">
F&lt;sup>21&lt;/sup> = F&lt;sup>16&lt;/sup>F&lt;sup>4&lt;/sup>F&lt;sup>1&lt;/sup>
&lt;/div>
&lt;/div>
&lt;p>And by doing that, we can calculate the nth Fibonacci number in only log&lt;sub>2&lt;/sub>(n) steps.&lt;/p>
&lt;aside>
&lt;p>If you really want to talk about &amp;ldquo;dynamic programming,&amp;rdquo; now&amp;rsquo;s the time &amp;ndash; we broke a harder operation into a series of &lt;em>shared subcomputations&lt;/em>. And we did it in constant space!&lt;/p>
&lt;/aside>
&lt;p>Okay, so that&amp;rsquo;s fun and all, but that&amp;rsquo;s not really what this blog post is about.&lt;/p>
&lt;p>I don&amp;rsquo;t know about you, but if I came across this matrix in the wild, I would not think &amp;ldquo;Oh, that&amp;rsquo;s the Fibonacci sequence&amp;rdquo;:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>I would probably think &amp;ldquo;huh, I dunno, it&amp;rsquo;s like, a reflection, sort of, or maybe a shear; what&amp;rsquo;s a shear again, hang on, I need to see a picture.&amp;rdquo;&lt;/p>
&lt;p>That is, I am used to thinking of matrices as transformations of &lt;em>points in space&lt;/em> &amp;ndash; scales and rotations and things like that. I&amp;rsquo;m not really used to thinking of matrices as &amp;ldquo;state machines.&amp;rdquo;&lt;/p>
&lt;p>But this duality is the beauty of linear algebra! Matrices are transformations of points in space and graphs and state machines all at the same time.&lt;/p>
&lt;p>So let&amp;rsquo;s take a look at the Fibonacci &lt;em>transformation&lt;/em>, applied to arbitrary points in R&lt;sup>2&lt;/sup>:&lt;/p>
&lt;p class="canvas-container">&lt;canvas id="transform" width="384" height="256">&lt;/canvas>&lt;/p>
&lt;p>That animation is progressively applying and removing the transformation, so we can get some intuition for how it deforms a square. But we&amp;rsquo;re really more interested in repeated applications of the transformation. So let&amp;rsquo;s start with the same points, but multiply by that same matrix over and over:&lt;/p>
&lt;p class="canvas-container">&lt;canvas id="transform2" width="384" height="256">&lt;/canvas>&lt;/p>
&lt;p>Interesting. Over time, they have a tendency to stretch out along the long diagonals of this rhombus. Let&amp;rsquo;s zoom out:&lt;/p>
&lt;p class="canvas-container">&lt;canvas id="transform3" width="384" height="256">&lt;/canvas>&lt;/p>
&lt;p>Every time a point reflects over that diagonal, it reflects at a slightly different angle, slowly converging towards this straight line.&lt;/p>
&lt;p>You might already have an idea of what that straight line means. You might know that, if you look at the ratio between subsequent Fibonacci numbers, they approximate the &lt;em>golden ratio&lt;/em>:&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;/p>
&lt;pre>&lt;code>1 / 1 = 1
2 / 1 = 2
3 / 2 = 1.5
5 / 3 = 1.666...
8 / 5 = 1.6
13 / 8 = 1.625
21 / 13 = 1.61538462
34 / 21 = 1.61904762
&lt;/code>&lt;/pre>
&lt;p>The golden ratio is irrational, but every subsequent Fibonacci number is a better and better rational approximation. (The golden ratio is around 1.618033988749 &amp;ndash; so we&amp;rsquo;re already pretty close.)&lt;/p>
&lt;p>It&amp;rsquo;s interesting to see that these estimations don&amp;rsquo;t &amp;ldquo;sneak up&amp;rdquo; on the golden ratio. In fact they alternate between over- and under-estimating it. Which is exactly what we saw in our visualization!&lt;/p>
&lt;p>If you return to the &amp;ldquo;state machine&amp;rdquo; interpretation of our matrix, remember that the value we&amp;rsquo;re plotting as &lt;code>x&lt;/code> is really &amp;ldquo;the current Fibonacci number,&amp;rdquo; and the value we&amp;rsquo;re plotting as &lt;code>y&lt;/code> is &amp;ldquo;the previous Fibonacci number.&amp;rdquo; So the ratio between successive numbers &amp;ndash; &lt;code>x/y&lt;/code> &amp;ndash; is just the slope of the lines that our points are traveling along. And we could see points reflecting over that diagonal, over- and under-shooting it, slowly converging&amp;hellip; towards the line whose slope is the golden ratio.&lt;/p>
&lt;p>Which is, in fact, the &amp;ldquo;long diagonal&amp;rdquo; of our rhombus.&lt;/p>
&lt;p>And this makes sense, I think &amp;ndash; this isn&amp;rsquo;t some weird coincidence. The golden ratio is all about the ratio between parts and wholes being the same as ratio between parts. And the Fibonacci sequence is all about adding together parts to become wholes that become parts in the next number of the sequence.&lt;/p>
&lt;p>Here, our two parts are the &amp;ldquo;current&amp;rdquo; and &amp;ldquo;previous&amp;rdquo; values, and the whole that they make is the &amp;ldquo;next&amp;rdquo; Fibonacci number. Even if we start with two numbers that are completely unrelated to the Fibonacci sequence &amp;ndash; say, &lt;code>8&lt;/code> and &lt;code>41&lt;/code> &amp;ndash; the simple way that we pick the next number will cause us to approximate the golden ratio after only a few iterations:&lt;/p>
&lt;pre>&lt;code>8 / 41 = 0.1951219
(8 + 41 = 49) / 8 = 6.125
(49 + 8 = 57) / 49 = 1.16326531
(57 + 49 = 106) / 57 = 1.85964912
(106 + 57 = 163) / 106 = 1.53773585
&lt;/code>&lt;/pre>
&lt;p>Why is that? Well, because of the definition of the golden ratio.&lt;/p>
&lt;div class="inline-math">
&lt;div class="content">
(A + B) / A = A / B = φ
&lt;/div>
&lt;/div>
&lt;p>This is &lt;em>extremely&lt;/em> unrigorous, but I can try to sketch out a very informal argument for why this is:&lt;/p>
&lt;p>Let&amp;rsquo;s say the ratio between &lt;code>A&lt;/code> and &lt;code>B&lt;/code> is some unknown quantity &lt;code>S&lt;/code>. It&amp;rsquo;s not the golden ratio, it might not be anywhere near the golden ratio; we have no idea what it is. In my 8 and 41 example, it wasn&amp;rsquo;t even in the right ballpark.&lt;/p>
&lt;div class="inline-math">
&lt;div class="content">A/B = S&lt;/div>
&lt;div class="content">(A + B) / A = (1 + B / A) = 1 + (1/S)&lt;/div>
&lt;/div>
&lt;p>So the ratio between the next element in our series and A will be &lt;code>(1 + (1/S))&lt;/code>.&lt;/p>
&lt;p>We still don&amp;rsquo;t know what &lt;code>S&lt;/code> is! But if we do this &lt;em>again&lt;/em>&amp;hellip;&lt;/p>
&lt;div class="inline-math">
&lt;div class="content">A' / B' = 1 + (1/S)&lt;/div>
&lt;div class="content">(A' + B') / A' =&lt;/div>
&lt;div class="content">(1 + (B' / A')) =&lt;/div>
&lt;div class="content">1 + (1 / (1 + (1 / S)))&lt;/div>
&lt;/div>
&lt;p>After each iteration, the original &lt;code>S&lt;/code> will become a smaller and smaller component in the final answer, until eventually we&amp;rsquo;ll just have an expression that looks like this:&lt;/p>
&lt;div class="inline-math">
&lt;div class="content">1 + (1 / (1 + (1 / (1 + (1 / (1 + (1 / ...)))))))&lt;/div>
&lt;/div>
&lt;p>Whatever our original &lt;code>S&lt;/code> was, its contribution to the final result will eventually be negligible. Even after just a few iterations, we can see that the choice of &lt;code>S&lt;/code> doesn&amp;rsquo;t make a huge difference in the outcome:&lt;/p>
&lt;div class="inline-math">
&lt;div class="content">1 + (1 / (1 + (1 / (1 + (1 / (1 + (1 / -5000))))))) = 1.6667&lt;/div>
&lt;div class="content">1 + (1 / (1 + (1 / (1 + (1 / (1 + (1 / 0.001))))))) = 1.5002&lt;/div>
&lt;/div>
&lt;p>And of course even that will fade away after a few more steps.&lt;/p>
&lt;p>In fact the version of that expression with an infinite number of steps &amp;ndash; where there is no &lt;code>S&lt;/code> at all, but just an infinite sequence of divisions &amp;ndash; is the &amp;ldquo;continued fraction&amp;rdquo; expression of the golden ratio.&lt;/p>
&lt;p>Except, well, I&amp;rsquo;m lying here.&lt;/p>
&lt;p>That residue will not fade away for &lt;em>all&lt;/em> values of &lt;code>S&lt;/code>. First of all, if &lt;code>S&lt;/code> is zero, it doesn&amp;rsquo;t matter how small that term gets &amp;ndash; you&amp;rsquo;re not going to squeeze a number out of it.&lt;/p>
&lt;p>But there is another, more interesting value of &lt;code>S&lt;/code> that breaks this rule. There is &lt;em>one&lt;/em> other number that will not tend towards 1.618 when you repeatedly take its reciprocal and add one. It is the number that is already one plus its own reciprocal:&lt;/p>
&lt;div class="inline-math">
&lt;div class="content">1 + (1 / 1.61803399) = 1.61803399&lt;/div>
&lt;/div>
&lt;p>Oh, gosh, yes, the golden ratio is one plus its own reciprocal. But I was talking about the &lt;em>other&lt;/em> number with that property:&lt;/p>
&lt;div class="inline-math">
&lt;div class="content">1 + (1 / -0.61803399) = -0.61803399&lt;/div>
&lt;/div>
&lt;p>This number is (1 - φ), and it is also -φ&lt;sup>-1&lt;/sup>. The golden ratio is weird like that.&lt;/p>
&lt;p>That number is a weird number, because if we have two numbers with that ratio &amp;ndash; say, &lt;code>-1.236&lt;/code> and &lt;code>2&lt;/code> &amp;ndash; and we applied our transformation, those points would not spread their wings towards the diagonal. What would they do instead?&lt;/p>
&lt;p class="canvas-container">&lt;canvas id="transform4" width="384" height="256">&lt;/canvas>&lt;/p>
&lt;p>Aha. Well, that makes sense.&lt;/p>
&lt;p>Some points tend towards the top right, some points tend towards the bottom left, but some points get &lt;em>stuck&lt;/em>. Sucked into the origin, cursed to forever travel along this one straight line.&lt;/p>
&lt;p>Points along the long diagonal also travel in a straight line &amp;ndash; they don&amp;rsquo;t bounce over the diagonal, because they&amp;rsquo;re already on it. Let&amp;rsquo;s just focus on these perfectly straight lines:&lt;/p>
&lt;p class="canvas-container">&lt;canvas id="transform5" width="384" height="256">&lt;/canvas>&lt;/p>
&lt;p>Not all matrices will produce straight lines like this when you apply them repeatedly. A rotation matrix, for example, will always change the direction of every single line each time you multiply a point by it.&lt;/p>
&lt;p>These straight lines are called &lt;em>eigenvectors&lt;/em>, which is German&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> for something like &amp;ldquo;intrinsic vector&amp;rdquo; or &amp;ldquo;characteristic vector.&amp;quot;&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>&lt;/p>
&lt;p>Well, to be more precise, any particular point on those straight lines is an &amp;ldquo;eigenvector.&amp;rdquo; The vector &lt;code>[φ 1]&lt;/code> is an eigenvector, and so is &lt;code>[-2.1φ -2.1]&lt;/code>. And the vector &lt;code>[-1/φ 1]&lt;/code> is an eigenvector, and so is &lt;code>[-2/φ 2]&lt;/code>.&lt;/p>
&lt;p>But all of the eigenvectors on each line are &amp;ldquo;similar,&amp;rdquo; so I&amp;rsquo;m just going to pick &lt;code>[φ 1]&lt;/code> and &lt;code>[(1-φ) 1]&lt;/code> as our two representative eigenvectors.&lt;/p>
&lt;p>When you multiply an eigenvector of a matrix by the matrix itself, you get back a new eigenvector on &amp;ldquo;the same line.&amp;rdquo; That is to say, you get back another eigenvector that is just some scalar multiple of the original eigenvector.&lt;/p>
&lt;p>For example, when we multiply our first eigenvector by the Fibonacci matrix:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>φ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>φ + 1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>φ&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>Well&amp;hellip; it&amp;rsquo;s not &lt;em>obvious&lt;/em> that this is the case, but we actually just scaled the vector by φ. Because φ&lt;sup>2&lt;/sup> = φ + 1. The golden ratio is weird.&lt;/p>
&lt;p>Similarly:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1 - φ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>2 - φ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1 - φ&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>We scaled it by (1 - φ), again somewhat cryptically:&lt;/p>
&lt;div class="inline-math">
&lt;div class="content">(1 - φ)(1 - φ) =&lt;/div>
&lt;div class="content">(1 - 2φ + φ&lt;sup>2&lt;/sup>) =&lt;/div>
&lt;div class="content">(1 - 2φ + φ + 1) =&lt;/div>
&lt;div class="content">(2 - φ)&lt;/div>
&lt;/div>
&lt;p>So when we multiply our Fibonacci matrix with its eigenvectors, we scale those numbers by φ and (1 - φ). These scaling factors are called &amp;ldquo;eigenvalues,&amp;rdquo; and it&amp;rsquo;s &lt;em>weird&lt;/em> that they look so much like the eigenvectors. That&amp;rsquo;s&amp;hellip; that&amp;rsquo;s a weird Fibonacci coincidence, a weird golden ratio thing, and not a general pattern that holds for eigenvectors and eigenvalues in general.&lt;/p>
&lt;p>Okay, so why do we care about this?&lt;/p>
&lt;p>Well, once we know the eigenvectors and eigenvalues of the matrix, we can actually perform repeated matrix multiplication in &lt;em>constant&lt;/em> time.&lt;/p>
&lt;p>&lt;em>&amp;hellip;Sort of&lt;/em>. You have to imagine a big asterisk after that sentence, which I will explain below.&lt;/p>
&lt;p>To explain how, we&amp;rsquo;re going to need to do a little bit of linear algebra. But first, I just want to restate everything I&amp;rsquo;ve said so far in explicit notation:&lt;/p>
&lt;p>Multiplying F with each eigenvector is the same as multiplying that eigenvector by its corresponding eigenvalue. So:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;span style="color:var(--palette-yellow)">φ&lt;/span>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;span style="color:var(--palette-yellow)">1&lt;/span>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div>&lt;span style="color:var(--palette-orange)">φ&lt;/span>&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;span style="color:var(--palette-yellow)">φ&lt;/span>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;span style="color:var(--palette-yellow)">1&lt;/span>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>And:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;span style="color:var(--palette-magenta)">1 - φ&lt;/span>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;span style="color:var(--palette-magenta)">1&lt;/span>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div>&lt;span style="color:var(--palette-purple)">(1 - φ)&lt;/span>&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;span style="color:var(--palette-magenta)">1 - φ&lt;/span>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;span style="color:var(--palette-magenta)">1&lt;/span>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>Right. But there&amp;rsquo;s actually a way to write those two equalities as a single equality:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;span style="color:var(--palette-yellow)">φ&lt;/span>&lt;/td>
&lt;td>&lt;span style="color:var(--palette-magenta)">1 - φ&lt;/span>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;span style="color:var(--palette-yellow)">1&lt;/span>&lt;/td>
&lt;td>&lt;span style="color:var(--palette-magenta)">1&lt;/span>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;span style="color:var(--palette-yellow)">φ&lt;/span>&lt;/td>
&lt;td>&lt;span style="color:var(--palette-magenta)">1 - φ&lt;/span>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;span style="color:var(--palette-yellow)">1&lt;/span>&lt;/td>
&lt;td>&lt;span style="color:var(--palette-magenta)">1&lt;/span>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;span style="color:var(--palette-orange)">φ&lt;/span>&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0&lt;/td>
&lt;td>&lt;span style="color:var(--palette-purple)">1 - φ&lt;/span>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>Instead of writing out each eigenvector as a separate column vector, I stuck them into a matrix. And instead of scaling each one by a scalar, I multiplied that matrix by a diagonal matrix.&lt;/p>
&lt;p>This is the same statement, though: right-multiplication by a diagonal matrix just means &amp;ldquo;scale the columns of the left matrix by the corresponding diagonal value.&amp;rdquo; We can gut check this by performing the multiplication, and seeing that we&amp;rsquo;re making the exact same statements as before:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>φ&lt;/td>
&lt;td>1 - φ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>φ + 1&lt;/td>
&lt;td>2 - φ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>φ&lt;/td>
&lt;td>1 - φ&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>φ&lt;/td>
&lt;td>1 - φ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>φ&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0&lt;/td>
&lt;td>1 - φ&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>φ + 1&lt;/td>
&lt;td>2 - φ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>φ&lt;/td>
&lt;td>1 - φ&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>But now we&amp;rsquo;re making these statement about both eigenvectors in parallel.&lt;/p>
&lt;p>This equality &amp;ndash; this statement about how multiplication by the Fibonacci matrix scales eigenvectors &amp;ndash; is the secret to computing Fibonacci numbers in &amp;ldquo;constant time&amp;rdquo;:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>φ&lt;/td>
&lt;td>1 - φ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>φ&lt;/td>
&lt;td>1 - φ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>φ&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0&lt;/td>
&lt;td>1 - φ&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>The trick here is that we&amp;rsquo;re going to right-multiply both sides of the equation by the inverse of our eigenvector matrix. This will eliminate it from the left-hand side entirely:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>φ&lt;/td>
&lt;td>1 - φ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>φ&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0&lt;/td>
&lt;td>1 - φ&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>φ&lt;/td>
&lt;td>1 - φ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="pow">-1&lt;/div>
&lt;/div>
&lt;p>And now we have a new way to calculate the &amp;ldquo;next Fibonacci number.&amp;rdquo; Previously we knew how to do it by multiplying with the matrix &lt;code>F&lt;/code>. Now we can do it by multiplying with, uhh, this inverse eigenvector matrix thing, and then the diagonal matrix of eigenvalues, and then the non-inverse matrix-of-eigenvectors.&lt;/p>
&lt;p>Much simpler, right?&lt;/p>
&lt;p>This is getting really long and complicated and I&amp;rsquo;m going to run out of space soon, so let&amp;rsquo;s give these things names:&lt;/p>
&lt;div class="math">
&lt;div>F&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;div class="math">
&lt;div>Q&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>φ&lt;/td>
&lt;td>1 - φ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;div class="math">
&lt;div>Λ&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>φ&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0&lt;/td>
&lt;td>1 - φ&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>That&amp;rsquo;s an upper-case lambda, and look, it&amp;rsquo;s just the convention for the eigenvalue matrix. Eigenvalues are called λ, and when you put them in a diagonal matrix you call it Λ. I don&amp;rsquo;t make the rules here.&lt;/p>
&lt;p>Now that we have some abbreviations, we can write that as the much more palatable:&lt;/p>
&lt;div class="inline-math">
&lt;div class="content">
&lt;div>F = Q Λ Q&lt;sup>-1&lt;/sup>&lt;/div>
&lt;/div>
&lt;/div>
&lt;p>Now, the whole reason that we&amp;rsquo;re doing this is to take advantage of another trick of associativity:&lt;/p>
&lt;div class="inline-math">
&lt;div class="content">
&lt;div>F&lt;sup>2&lt;/sup> = (Q Λ Q&lt;sup>-1&lt;/sup>)(Q Λ Q&lt;sup>-1&lt;/sup>)&lt;/div>
&lt;/div>
&lt;/div>
&lt;div class="inline-math">
&lt;div class="content">
&lt;div>F&lt;sup>2&lt;/sup> = Q Λ (Q&lt;sup>-1&lt;/sup>Q) Λ Q&lt;sup>-1&lt;/sup>&lt;/div>
&lt;/div>
&lt;/div>
&lt;div class="inline-math">
&lt;div class="content">
&lt;div>F&lt;sup>2&lt;/sup> = Q Λ Λ Q&lt;sup>-1&lt;/sup>&lt;/div>
&lt;/div>
&lt;/div>
&lt;div class="inline-math">
&lt;div class="content">
&lt;div>F&lt;sup>2&lt;/sup> = Q Λ&lt;sup>2&lt;/sup> Q&lt;sup>-1&lt;/sup>&lt;/div>
&lt;/div>
&lt;/div>
&lt;p>That was very abstract, so take a second to think about what this &lt;em>means&lt;/em>. F&lt;sup>2&lt;/sup> is the matrix that calculates two steps of our Fibonacci state machine. And we can use this same trick to calculate &lt;em>any&lt;/em> power of F, just by calculating powers of Λ.&lt;/p>
&lt;div class="inline-math">
&lt;div class="content">
&lt;div>F&lt;sup>n&lt;/sup> = Q Λ&lt;sup>n&lt;/sup> Q&lt;sup>-1&lt;/sup>&lt;/div>
&lt;/div>
&lt;/div>
&lt;p>And this is good, because Λ is a &lt;em>diagonal&lt;/em> matrix. And it&amp;rsquo;s really easy to exponentiate a diagonal matrix! You just exponentiate each element of its diagonal. We don&amp;rsquo;t even need to use repeated squaring.&lt;/p>
&lt;p>This means that we can actually calculate arbitrary powers of F in &lt;em>constant&lt;/em> time&amp;hellip; if we pretend that exponentiation of a scalar is a constant time operation.&lt;/p>
&lt;p>It&amp;rsquo;s not, though. I mean, yes, exponentiation of an IEEE 754 64-bit floating-point number &lt;em>is&lt;/em> constant time, but that&amp;rsquo;s not what we said. We&amp;rsquo;re talking about exponentiating an irrational number, and my computer can only represent approximations of that number, and that floating-point error adds up fast. So in order to actually use this to compute large Fibonacci numbers, we would need to use arbitrary-precision floating point, and exponentiating arbitrary precision values is &lt;em>not&lt;/em> constant time. It&amp;rsquo;s&amp;hellip; I don&amp;rsquo;t know, probably logarithmic? But like both to the exponent and the size of the result, and the size of the result is increasing exponentially, so it nets out to linear? I don&amp;rsquo;t actually know.&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>&lt;/p>
&lt;p>But I don&amp;rsquo;t want to spoil the fun. This is still a very interesting trick, and it&amp;rsquo;s worth understanding how it works, even if it doesn&amp;rsquo;t actually give us a way to compute arbitrarily large Fibonacci numbers in constant time.&lt;/p>
&lt;p>So: what are we doing.&lt;/p>
&lt;p>We moved a bunch of symbols around, and we wound up with this expression:&lt;/p>
&lt;div class="inline-math">
&lt;div class="content">
&lt;div>F&lt;sup>n&lt;/sup> = Q Λ&lt;sup>n&lt;/sup> Q&lt;sup>-1&lt;/sup>&lt;/div>
&lt;/div>
&lt;/div>
&lt;p>But I don&amp;rsquo;t really know what Q&lt;sup>-1&lt;/sup> means, and it&amp;rsquo;s not really clear to me why I should care. Why is multiplying by these three weird matrices the same as multiplying by F? What, intuitively, are we doing here?&lt;/p>
&lt;p>At a high level, we&amp;rsquo;re translating points into a different coordinate system, then doing something to it, and then translating them back into our original coordinate system.&lt;/p>
&lt;p>You already know that we can write any point in space as a vector &amp;ndash; X and Y coordinates. That&amp;rsquo;s what we&amp;rsquo;ve been doing this whole time.&lt;/p>
&lt;p>But we can &lt;em>also&lt;/em> write a point in space as the sum of two other vectors. Like, &lt;code>[5 3]&lt;/code>. We could write that as &lt;code>[1 2] + [4 1]&lt;/code> instead. Which, okay, sure. That&amp;rsquo;s not very interesting.&lt;/p>
&lt;p>One &amp;ldquo;interesting&amp;rdquo; way to write &lt;code>[5 3]&lt;/code> is as the sum of these two vectors: &lt;code>[5 0] + [0 3]&lt;/code>. Or, to say that another way:&lt;/p>
&lt;div class="math">
&lt;div>5&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>+&lt;/div>
&lt;div>3&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>This is interesting because &lt;code>[1 0]&lt;/code> and &lt;code>[0 1]&lt;/code> are basically the &amp;ldquo;X axis&amp;rdquo; and &amp;ldquo;Y axis.&amp;rdquo; And we can think of the point &lt;code>[5 3]&lt;/code> as a (trivial!) linear combination of these two axes.&lt;/p>
&lt;p>But we could pick &lt;em>different&lt;/em> axes. We can pick any vectors we want as our axes,&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup> so let&amp;rsquo;s pretend for a moment that our axes are &lt;code>[1 1]&lt;/code> and &lt;code>[1 -1]&lt;/code> instead. Which means that we would write &lt;code>[5 3]&lt;/code> as:&lt;/p>
&lt;div class="math">
&lt;div>4&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>+&lt;/div>
&lt;div>1&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>Or, to write that another way:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>-1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>Now we can think of this vector-of-coefficients, &lt;code>[4 1]&lt;/code>, as another way to identify the point in space &lt;code>x=5 y=3&lt;/code> when we we&amp;rsquo;re pretending that our axes are &lt;code>[1 1]&lt;/code> and &lt;code>[1 -1]&lt;/code>. Except in linear algebra we&amp;rsquo;d call these &amp;ldquo;basis vectors&amp;rdquo; instead of &amp;ldquo;axes.&amp;rdquo;&lt;/p>
&lt;p>But how did we find the coefficients &lt;code>[4 1]&lt;/code>? Well, I just found that one by hand; it was pretty easy. But &lt;em>in general&lt;/em>, if we want to express some other &lt;em>point&lt;/em> using these basis vectors &amp;ndash; let&amp;rsquo;s say &lt;code>[63 -40]&lt;/code> &amp;ndash; we&amp;rsquo;ll need to solve an equation that looks like this:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>-1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>?&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>?&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>63&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-40&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>And we can do that by, you know, regular algebra. We &amp;ldquo;divide&amp;rdquo; both sides by our matrix-of-basis-vectors, by left-multiplying with the inverse matrix:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>-1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="sub">-1&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>-1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>?&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>?&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>-1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="sub">-1&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>63&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-40&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>And after the inverses cancel, we&amp;rsquo;re left with the following formula:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>?&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>?&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>-1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="sub">-1&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>63&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-40&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>And the problem reduces to matrix inversion.&lt;/p>
&lt;p>Now, I don&amp;rsquo;t know about you, but I don&amp;rsquo;t remember how to invert a matrix. I know there&amp;rsquo;s a formula in two dimensions, but the only thing I remember about it is that it involves calculating the determinant, and I forgot how to do that too. So let&amp;rsquo;s just ask a computer to invert it for us:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>?&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>?&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0.5&lt;/td>
&lt;td>0.5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0.5&lt;/td>
&lt;td>-0.5&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>63&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-40&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>Hmm. I feel like I probably could&amp;rsquo;ve worked that out myself.&lt;/p>
&lt;p>But that lets us solve the equation, and figure out how to write the point &lt;code>[63 -40]&lt;/code> as a combination of the vectors &lt;code>[1 1]&lt;/code> and &lt;code>[1 -1]&lt;/code>:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0.5&lt;/td>
&lt;td>0.5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0.5&lt;/td>
&lt;td>-0.5&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>63&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-40&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>51.5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>11.5&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>Great! We did it.&lt;/p>
&lt;p>And &lt;em>here&amp;rsquo;s why we care:&lt;/em>&lt;/p>
&lt;p>We can use this exact same trick to write down the points in our Fibonacci sequence as a linear combination of our two eigenvectors. Like this:&lt;/p>
&lt;p class="canvas-container">&lt;canvas id="bases" width="384" height="256">&lt;/canvas>&lt;/p>
&lt;p>Click or tap to add points there, to see how we can write each point in space as a combination of the &amp;ldquo;short diagonal&amp;rdquo; and &amp;ldquo;long diagonal&amp;rdquo; eigenvectors of our matrix.&lt;/p>
&lt;p>Normally to identify a point in space we would give its XY coordinates: go this far along the X-axis, then this far along the Y-axis. But here we&amp;rsquo;re representing points in &amp;ldquo;φ&amp;rdquo; and &amp;ldquo;1 - φ&amp;rdquo; coordinates: go this far along the short diagonal, then this far along the long diagonal.&lt;/p>
&lt;p>But how do we know how far to go along these diagonals? Well, we &amp;ldquo;divide by&amp;rdquo; the eigenvectors. In other words, we have to compute the inverse of this matrix:&lt;/p>
&lt;div class="math">
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>φ&lt;/td>
&lt;td>1 - φ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="pow">-1&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="ratio">
&lt;div>1&lt;/div>
&lt;hr />
&lt;div>√5&lt;/div>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>φ - 1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-1&lt;/td>
&lt;td>φ&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;aside>
&lt;p>(φ - 1) isn&amp;rsquo;t a typo &amp;ndash; that&amp;rsquo;s -(1 - φ), or φ&lt;sup>-1&lt;/sup>. The golden ratio is &lt;em>weird&lt;/em>.&lt;/p>
&lt;/aside>
&lt;p>Now, matrix inversion is boring, so I&amp;rsquo;m just presenting the answer here. This inverse matrix is how we can convert from &amp;ldquo;XY coordinates&amp;rdquo; into &amp;ldquo;eigenvector coordinates.&amp;rdquo;&lt;/p>
&lt;p>Let&amp;rsquo;s work through a concrete example to make sure this works.&lt;/p>
&lt;p>&lt;code>[8 5]&lt;/code> is a point on the Fibonacci sequence. We can express that as a combination of eigenvectors instead:&lt;/p>
&lt;div class="math">
&lt;div class="ratio">
&lt;div>1&lt;/div>
&lt;hr />
&lt;div>√5&lt;/div>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>φ - 1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-1&lt;/td>
&lt;td>φ&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>≈&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>4.96&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0.04&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>4.96 and 0.04 are the coefficients we will pair with our eigenvectors: we have to travel 4.96 units down the long diagonal, and 0.04 units along the short diagonal to arrive at the point &lt;code>[8 5]&lt;/code>.&lt;/p>
&lt;div class="math">
&lt;div>4.96&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Φ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>+&lt;/div>
&lt;div>0.04&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1 - Φ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>≈&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>8.025&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4.96&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>+&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>-0.025&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0.04&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>Great. It worked!&lt;/p>
&lt;p>But that wasn&amp;rsquo;t very interesting &amp;ndash; we just converted our point into the eigenvector basis and then right back into the normal XY basis. It was kind of a pointless transformation.&lt;/p>
&lt;p>But we don&amp;rsquo;t have to do the unconversion immediately. We can keep the point in this &amp;ldquo;eigenbasis&amp;rdquo; for a little while, and do stuff to the vector-of-coefficients, and &lt;em>then&lt;/em> convert it back.&lt;/p>
&lt;p>Specifically, we can scale the coefficients by the eigenvalues of our Fibonacci matrix. We can multiply the &amp;ldquo;long diagonal&amp;rdquo; component by Φ&lt;sup>2&lt;/sup>, and multiply the short diagonal component by (1 - Φ)&lt;sup>2&lt;/sup>, and we&amp;rsquo;ll have a new point: something close to &lt;code>[12.985 0.015]&lt;/code>. And if we convert that back into XY coordinates:&lt;/p>
&lt;div class="math">
&lt;div>12.985&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Φ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>+&lt;/div>
&lt;div>0.015&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1 - Φ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;div>=&lt;/div>
&lt;div class="matrix">
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>21&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>13&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;p>We just advanced our point two more steps along the Fibonacci sequence, with nothing more than scalar exponentiation and a constant number of vector operations.&lt;/p>
&lt;p>This is exactly the same as the expression:&lt;/p>
&lt;div class="inline-math">
&lt;div class="content">
&lt;div>F&lt;sup>2&lt;/sup> = Q Λ&lt;sup>2&lt;/sup> Q&lt;sup>-1&lt;/sup>&lt;/div>
&lt;/div>
&lt;/div>
&lt;p>But as someone with no background in linear algebra, I find it easy to get lost in the notation, so it&amp;rsquo;s easier for me to think about this as operations on separate column vectors rather than as operations on matrices. Even though they are the same thing.&lt;/p>
&lt;p>Of course, calculating two steps of the Fibonacci sequence in constant time isn&amp;rsquo;t that impressive. But we can do the same with Φ&lt;sup>1000&lt;/sup>, and use that to calculate the thousandth Fibonacci number in constant time.&lt;/p>
&lt;p>&amp;hellip;Assuming we could calculate Φ&lt;sup>1000&lt;/sup> in constant time. Which we can&amp;rsquo;t, in real life.&lt;/p>
&lt;hr>
&lt;p>Alright.&lt;/p>
&lt;p>The post is over; you saw the trick. &amp;ldquo;Eigendecomposition,&amp;rdquo; this is called.&lt;/p>
&lt;p>I glossed over a few steps &amp;ndash; I spent absolutely no time explaining &lt;em>how I knew&lt;/em> the eigenvalues and eigenvectors of this matrix, for example. I just asserted that they were related to the golden ratio. But in reality you can solve for them, or ask a computer to do it for you. It&amp;rsquo;s pretty mechanical, like matrix inversion &amp;ndash; it seems linear algebra is best explored with a repl nearby.&lt;/p>
&lt;p>In any case, I think that the &lt;em>why&lt;/em> of eigendecomposition is more interesting than the &lt;em>how&lt;/em>.&lt;/p>
&lt;p>As for the Fibonacci sequence&amp;hellip; well, this is a pretty terrible way to actually calculate Fibonacci numbers. Even if we pretend that we only care about numbers that can fit in IEEE 754 double-precision floats, we &lt;em>still&lt;/em> can&amp;rsquo;t use this technique to calculate very many Fibonacci numbers, because the floating-point error adds up too quickly.&lt;/p>
&lt;p>But if we only care about double-precision floats&amp;hellip; well, there is one more Fibonacci implementation to consider. It&amp;rsquo;s an algorithm that that runs in constant time, and constant space, and covers the full gamut of floating-point numbers without accumulating any error at all&amp;hellip;&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="kr">const&lt;/span> &lt;span class="nx">fibs&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">13&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">21&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">34&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">55&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">89&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">144&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">233&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">377&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">610&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">987&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1597&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2584&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">4181&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">6765&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10946&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">17711&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">28657&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">46368&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">75025&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">121393&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">196418&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">317811&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">514229&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">832040&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1346269&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2178309&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3524578&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5702887&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">9227465&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">14930352&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">24157817&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">39088169&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">63245986&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">102334155&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">165580141&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">267914296&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">433494437&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">701408733&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1134903170&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1836311903&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2971215073&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">4807526976&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">7778742049&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">12586269025&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">20365011074&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">32951280099&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">53316291173&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">86267571272&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">139583862445&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">225851433717&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">365435296162&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">591286729879&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">956722026041&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1548008755920&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2504730781961&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">4052739537881&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">6557470319842&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10610209857723&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">17167680177565&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">27777890035288&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">44945570212853&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">72723460248141&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">117669030460994&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">190392490709135&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">308061521170129&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">498454011879264&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">806515533049393&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1304969544928657&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2111485077978050&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3416454622906707&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5527939700884757&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">8944394323791464&lt;/span>&lt;span class="p">];&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">fib&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">n&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">fibs&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">n&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>But it&amp;rsquo;s more fun to overthink it.&lt;/p>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>In case you are one of today&amp;rsquo;s lucky 10,000: the golden ratio is also very close to the conversion rate between miles and kilometers, so you can use Fibonacci numbers to approximate conversions between miles and kilometers in your head. For example, 80km ≈ 50mi. This is a &lt;em>weirdly&lt;/em> good conversion &amp;ndash; the exact answer is 49.7097mi.&lt;/p>
&lt;p>It even works when you don&amp;rsquo;t have a round Fibonacci number to work with. 120kph is probably around 90% of 130kph, and 90% of 80mph is 72mph&amp;hellip; the correct answer would be 74.6mph, but we got a decent ballpark with nothing but eyeball math.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2" role="doc-endnote">
&lt;p>Er, the German word is Eigenvektor, so it&amp;rsquo;s like&amp;hellip; half a loan word? A loan sub-word? Whatever, the &amp;ldquo;eigen&amp;rdquo; part is the relevant bit here.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3" role="doc-endnote">
&lt;p>Straight lines are eigenvectors, but eigenvectors are not necessarily straight lines. Rotation matrices &lt;em>also&lt;/em> have eigenvectors, but they have &lt;em>complex&lt;/em> eigenvectors. Straight lines like this are &lt;em>real&lt;/em> eigenvectors.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4" role="doc-endnote">
&lt;p>The exact same argument applies to the &amp;ldquo;logarithmic&amp;rdquo; exponentiation-by-squaring algorithm as well &amp;ndash; squaring arbitrarily large numbers requires arbitrary precision multiplication. It feels different to me, though, because of floating point error: when you&amp;rsquo;re exponentiating eigenvalues, you need to use arbitrary precision arithmetic even when your final answer could fit into a double. But the integer squaring approach only needs bigints when the Fibonacci numbers themselves become too large to fit into words.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5" role="doc-endnote">
&lt;p>As long as the vectors we pick are &amp;ldquo;linearly independent&amp;rdquo; &amp;ndash; there&amp;rsquo;s no way to express &lt;code>[5 3]&lt;/code> as a combination of &lt;code>[1 0]&lt;/code> and &lt;code>[2 0]&lt;/code>, for example.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><pubDate>Sun, 30 Jul 2023 00:00:00 +0000</pubDate><link>https://ianthehenry.com/posts/fibonacci/</link><guid isPermaLink="true">https://ianthehenry.com/posts/fibonacci/</guid></item><item><title>My Kind of REPL</title><description>&lt;p>I want to tell you about an idea that has had a huge influence on the way that I write software. And I mean that in the literal sense: it&amp;rsquo;s changed the &lt;em>way&lt;/em> that I write software; it&amp;rsquo;s re-shaped my development workflow.&lt;/p>
&lt;p>The idea is this: you can write programs that modify themselves.&lt;/p>
&lt;p>And I don&amp;rsquo;t mean macros or metaprogramming or anything fancy like that. I mean that you can write programs that &lt;em>edit their own source code&lt;/em>. Like, the files themselves. The actual text files on disk that have your source code in them.&lt;/p>
&lt;p>That&amp;rsquo;s not the whole idea, though. There&amp;rsquo;s more to it: you write programs that can edit themselves, and then you &lt;em>use that as your REPL&lt;/em>.&lt;/p>
&lt;!-- more -->
&lt;p>Instead of typing something into a prompt and hitting enter and seeing the output on stdout, you type something into a file and hit some editor keybinding and the result gets &lt;em>inserted into the file itself&lt;/em>. Patched on disk, right next to the original expression, ready to be committed to source control.&lt;/p>
&lt;p>This read-eval-patch loop is already a little useful &amp;ndash; it&amp;rsquo;s a REPL with all of the conveniences of your favorite editor &amp;ndash; but we&amp;rsquo;re still not done. There&amp;rsquo;s one more part to the idea.&lt;/p>
&lt;p>Once you have your expression and your &amp;ldquo;REPL output&amp;rdquo; saved into the same file, you can &lt;em>repeat&lt;/em> that &amp;ldquo;REPL session&amp;rdquo; in the future, and you can &lt;em>see if the output ever changes&lt;/em>.&lt;/p>
&lt;p>And then you use this technique to write all of your tests.&lt;/p>
&lt;p>That&amp;rsquo;s the whole idea. You use your source files as persistent REPLs, and those REPL sessions become living, breathing test cases for the code you were inspecting. They let you know if anything &lt;em>changes&lt;/em> about any of the expressions that you&amp;rsquo;ve run in the past. This kind of REPL can&amp;rsquo;t tell you if the new results are &lt;em>right&lt;/em> or &lt;em>wrong&lt;/em>, of course &amp;ndash; just that they&amp;rsquo;re different. It&amp;rsquo;s still up to you to separate &amp;ldquo;test failures&amp;rdquo; from expected divergences, and to curate the expressions-under-observation into a useful set.&lt;/p>
&lt;p>This is pretty much the only way that I have written automated tests for the last six years. Except that it isn&amp;rsquo;t really &amp;ndash; this is the way that I &lt;em>haven&amp;rsquo;t&lt;/em> written automated tests. This is the way that I have caused automated tests to appear for free around me, by doing exactly what I have always done: writing code and then running it, and seeing if it did what I wanted.&lt;/p>
&lt;p>I&amp;rsquo;m exaggerating, but not as much as you might think. Of course these tests aren&amp;rsquo;t &lt;em>free:&lt;/em> it takes work to come up with interesting expressions, and I have to write setup code to actually test the thing I care about, and there is judgment and curation and naming-things involved. And of course there is some effort in verifying that the code under test actually does the thing that I wanted it to do.&lt;/p>
&lt;p>But the reason that it feels free is that nearly all of that is work that &lt;em>I would be doing already&lt;/em>.&lt;/p>
&lt;p>Like, forget about automated tests for a second &amp;ndash; when you write some non-trivial function, don&amp;rsquo;t you usually run it a couple times to see if it actually does the thing that you wanted it to do? I do. Usually by typing in interesting expressions at a REPL, and checking the output, and only moving on once I&amp;rsquo;m convinced that it works.&lt;/p>
&lt;p>And those expressions that I run to convince myself that the code works are &lt;em>exactly&lt;/em> the expressions that I want to run again in the future to convince myself that the code &lt;em>still&lt;/em> works after a refactor. They are exactly the expressions that I want to share with someone else to convince &lt;em>them&lt;/em> that the code works. They are, in other words, exactly the test cases that I care about.&lt;/p>
&lt;p>And yes, I mean, I&amp;rsquo;m still exaggerating a bit. There is a difference between expressions that you run once and expressions that you run a thousand times. In particular, you want your &amp;ldquo;persistent REPL&amp;rdquo; outputs to be relatively &lt;em>stable&lt;/em> &amp;ndash; you don&amp;rsquo;t want tests to fail because the order of keys in a hash table changed. And usually you want to document these cases in some way, to say &lt;em>why&lt;/em> a particular case is interesting, so that it&amp;rsquo;s easier to diagnose a failure in the future.&lt;/p>
&lt;p>But we&amp;rsquo;re getting a little into the weeds there. Let&amp;rsquo;s take a step back.&lt;/p>
&lt;p>You&amp;rsquo;ve heard the idea, now let&amp;rsquo;s see how it feels. I want to demonstrate the workflow to you, but it&amp;rsquo;s going to require a little bit of suspension of disbelief on your part: this is a blog post, and I don&amp;rsquo;t really have time to show you any particularly &lt;em>interesting&lt;/em> tests. I also have to confess that I don&amp;rsquo;t write very many interesting tests outside of work, so this will be a combination of completely fake contrived examples and trivial unit tests lifted from random side projects.&lt;/p>
&lt;p>With that in mind, let&amp;rsquo;s start with one from the &amp;ldquo;extremely contrived&amp;rdquo; camp:&lt;/p>
&lt;div class="video-container">
&lt;video controls
width="724"
height="478"
preload=metadata
poster="/posts/my-kind-of-repl/judge-poster_hude160a6d947bfd91a8fb2c82c7035457_229398_1448x956_fit_q75_h2_box_3.69645248eccf8d2e37cd87c561f4fad0cb83553d061e7990e9b67ce8e036e300.webp"
style="
background-size: cover;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAVCAIAAACor3u9AAACiElEQVR4nLxVy27UShDt7mq7257xPO69cXIjhMQOJETILmLPF/ABfCdfEYkVKxQCUUYZmHhitx/9qELOBBSFIQiS4WxctruqXHVOleXrFwcsd2&amp;#43;FkB/ZDSil9vf3j46OiAgR27ZNkgQArLVCCCklEYUQ5vP5Tc9vICL&amp;#43;bO/5cDhcLi8mkzFjfD4/67ruZw6/CwHAdx88REQhBCICQAjhvqIzxgBAZtlgNBHni2bIeaZj03nO2cLDdDRCTrOT07vmUDr1Idr6b7dDxpUSyQhBmarSWlfLwjp/l&amp;#43;g9VYwxU5ZEYjjQgeDk&amp;#43;Hj1bjab3fHbV5CrCzEGrEHi2XiIgUWxdl0zmUwW52dJMtAqNcaE4Dvrt/LtxpTW2khpgdWi&amp;#43;IUirkjeyQdlbU3lvpeGiD/We/3h2jM30JO8smZnZuf/4WAQpwIQQ1G5dJw3porjqG3b6fQfImrbJooia60NHrt68m8OAoqiAIBIyqYxPNLetaY0a1rUW1IQI0kiy2RREwaXb&amp;#43;eLLwulVNPUWmspJYAUwmdq0Mp4uSzqvm90LZpZU0Q2GhP1h5SOkFSiU&amp;#43;tFHMmqtolWAFA3rYqj1QBTL7TYOae1ZkwoHSuluWBaxURsbVflyz18c9jfXCw7RFsua&amp;#43;/9aspPXRd8IGK2axhjzrnLCngIQQgIwQNAHMfOOs&amp;#43;59&amp;#43;snlD95/Oj8wt3O1R8DAETVbCj4FQTjfLMJQIjNJrjf9bkmwUaj/40E8tUBHn5ISz/1zksuO28jiEzbr2vve&amp;#43;ErpUBCberb/3QhhCRJnHP8UjWc8zRNn&amp;#43;Yz&amp;#43;e4Tfi5cGwre66nfFh1rvXdE6H3gnDtniVgI/na2EPtlFQJy3tuXju498a8BAAD//yKTbeWcbt6XAAAAAElFTkSuQmCC);
">
&lt;source src="https://ianthehenry.com/posts/my-kind-of-repl/judge-1448x956.100994ea71205d171f5dd31d6b87ea8067ab0fb7e4e8179a235f702f6c2d666b.mp4" type="video/mp4">
&lt;/video>
&lt;/div>
&lt;p>Yes, I know that you haven&amp;rsquo;t actually implemented a sorting algorithm since the third grade, but I hope that it gives you the gist of the workflow: you write expressions, press a button, and the result appears directly in your source code. It&amp;rsquo;s like working in a Jupyter notebook, except that you don&amp;rsquo;t have to work in a Jupyter notebook. You don&amp;rsquo;t have to leave the comfort of Emacs, or Vim, or VSCode, or BBEdit &amp;ndash; as long as your editor can load files off a file system, it&amp;rsquo;s compatible with this workflow.&lt;/p>
&lt;p>In case you skipped the video, the end result looks something like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-clojure" data-lang="clojure">&lt;span class="p">(&lt;/span>&lt;span class="nf">use&lt;/span> &lt;span class="nv">judge&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="kd">defn &lt;/span>&lt;span class="nv">slow-sort&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nv">list&lt;/span>&lt;span class="p">]&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nf">case&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">length&lt;/span> &lt;span class="nv">list&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="mi">0&lt;/span> &lt;span class="nv">list&lt;/span>
&lt;span class="mi">1&lt;/span> &lt;span class="nv">list&lt;/span>
&lt;span class="mi">2&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">let &lt;/span>&lt;span class="p">[[&lt;/span>&lt;span class="nv">x&lt;/span> &lt;span class="nv">y&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="nv">list&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">[(&lt;/span>&lt;span class="nb">min &lt;/span>&lt;span class="nv">x&lt;/span> &lt;span class="nv">y&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">max &lt;/span>&lt;span class="nv">x&lt;/span> &lt;span class="nv">y&lt;/span>&lt;span class="p">)])&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nf">do&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">def &lt;/span>&lt;span class="nv">pivot&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">in&lt;/span> &lt;span class="nb">list &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">math/floor&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">/ &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">length&lt;/span> &lt;span class="nv">list&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">))))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">def &lt;/span>&lt;span class="nv">bigs&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">filter &lt;/span>&lt;span class="err">|&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">&amp;gt; &lt;/span>&lt;span class="nv">$&lt;/span> &lt;span class="nv">pivot&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nv">list&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">def &lt;/span>&lt;span class="nv">smalls&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">filter &lt;/span>&lt;span class="err">|&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">&amp;lt; &lt;/span>&lt;span class="nv">$&lt;/span> &lt;span class="nv">pivot&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nv">list&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">def &lt;/span>&lt;span class="nv">equals&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">filter &lt;/span>&lt;span class="err">|&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">= &lt;/span>&lt;span class="nv">$&lt;/span> &lt;span class="nv">pivot&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nv">list&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">[&lt;/span>&lt;span class="c1">;(slow-sort smalls) ;equals ;(slow-sort bigs)])))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nb">test &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">slow-sort&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">3&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">])&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nb">test &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">slow-sort&lt;/span> &lt;span class="p">[])&lt;/span> &lt;span class="p">[])&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nb">test &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">slow-sort&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Those tests at the end might look like regular equality assertions &amp;ndash; and, in a way, they are. But I only had to write the &amp;ldquo;first half&amp;rdquo; of them &amp;ndash; I only wrote the expressions to run in my &amp;ldquo;REPL&amp;rdquo; &amp;ndash; and my test framework filled in right-hand sides for me.&lt;/p>
&lt;p>Now, as you probably noticed, there were a lot of parentheses in that demo. Don&amp;rsquo;t be alarmed: the parentheses are entirely incidental. I just happen to be &lt;a href="https://ianthehenry.com/posts/why-janet/">spending a lot of time with Janet&lt;/a> lately, and I&amp;rsquo;ve written &lt;a href="https://github.com/ianthehenry/judge">a test framework&lt;/a> that&amp;rsquo;s heavily based around this workflow.&lt;/p>
&lt;p>But there&amp;rsquo;s nothing parentheses-specific about this technique. I first met this type of programming in OCaml, via the &lt;a href="https://github.com/janestreet/ppx_expect">&amp;ldquo;expect test&amp;rdquo; framework&lt;/a>. The specifics of OCaml&amp;rsquo;s expect tests are a little bit different, but the overall workflow is the same:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ocaml" data-lang="ocaml">&lt;span class="k">let&lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="n">expect_test&lt;/span> &lt;span class="s2">&amp;#34;formatting board coordinates&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span>
&lt;span class="n">print_endline&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">to_string&lt;/span> &lt;span class="o">{&lt;/span> &lt;span class="n">row&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">0&lt;/span>&lt;span class="o">;&lt;/span> &lt;span class="n">col&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">0&lt;/span> &lt;span class="o">});&lt;/span>
&lt;span class="o">[%&lt;/span>&lt;span class="n">expect&lt;/span> &lt;span class="s2">&amp;#34;A1&amp;#34;&lt;/span>&lt;span class="o">];&lt;/span>
&lt;span class="n">print_endline&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">to_string&lt;/span> &lt;span class="o">{&lt;/span> &lt;span class="n">row&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">10&lt;/span>&lt;span class="o">;&lt;/span> &lt;span class="n">col&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">4&lt;/span> &lt;span class="o">});&lt;/span>
&lt;span class="o">[%&lt;/span>&lt;span class="n">expect&lt;/span> &lt;span class="s2">&amp;#34;E11&amp;#34;&lt;/span>&lt;span class="o">]&lt;/span>
&lt;span class="o">;;&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Instead of the expression-oriented API we saw in Janet, an OCaml expect test is a series of &lt;em>statements&lt;/em> that print to stdout, followed by automatically-updating assertions about what exactly was printed. This makes sense because OCaml doesn&amp;rsquo;t really have a canonical way to print a representation of any value,&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> so you have to decide exactly what you want your REPL to Print.&lt;/p>
&lt;p>And this workflow works in normal languages too! Here&amp;rsquo;s an example in Rust, using the &lt;a href="https://github.com/aaronabramov/k9">K9 test framework&lt;/a>:&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="cp">#[test]&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">fn&lt;/span> &lt;span class="nf">indentation&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">k9&lt;/span>::&lt;span class="n">snapshot&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">tokenize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;
&lt;/span>&lt;span class="s">a
&lt;/span>&lt;span class="s"> b
&lt;/span>&lt;span class="s"> c
&lt;/span>&lt;span class="s">d
&lt;/span>&lt;span class="s"> e
&lt;/span>&lt;span class="s"> f
&lt;/span>&lt;span class="s">g
&lt;/span>&lt;span class="s">&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;a ␤ → b ␤ c ␤ ← d ␤ → e ␤ → f ␤ ← ← g ␤&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We&amp;rsquo;re pulling from the &amp;ldquo;trivial side project&amp;rdquo; category now. That&amp;rsquo;s a test I wrote for an indentation-sensitive tokenizer, but again, I just wrote an expression that I wanted to check and then checked that the result looked right. I didn&amp;rsquo;t have to spend any time crafting the token stream that I expected to see ahead of time &amp;ndash; I only had to write down an expression that seemed interesting to me.&lt;/p>
&lt;p>The fact that you don&amp;rsquo;t actually have to write the expected value might seem like a trivial detail &amp;ndash; it&amp;rsquo;s not &lt;em>that&lt;/em> hard to write your expectations down up front; people do it all the time &amp;ndash; but it&amp;rsquo;s actually an &lt;em>essential&lt;/em> part of this workflow. It&amp;rsquo;s an implausibly big deal. It is, in fact, the first beautiful thing I want to highlight about this technique:&lt;/p>
&lt;h1 id="1-when-its-this-easy-to-write-tests-you-write-more-of-them">1. When it&amp;rsquo;s this easy to write tests, you write more of them&lt;/h1>
&lt;p>The fact that I don&amp;rsquo;t have to type the expectation for my tests matters a lot more than it seems like it &lt;em>should&lt;/em>. Like, I&amp;rsquo;m an adult, right? I can walk through that code and say &amp;ldquo;a, newline, indent, b, newline&amp;hellip;&amp;rdquo; and I can write down the expected result. How hard can it be?&lt;/p>
&lt;p>I actually just timed myself doing that: it took almost exactly 30 seconds to read this input and write out the expected output by hand (sans Unicode). 30 seconds doesn&amp;rsquo;t sound very long, but it&amp;rsquo;s &lt;em>friction&lt;/em>. It was a &lt;em>boring&lt;/em> 30 seconds. It&amp;rsquo;s something that I want to minimize. And what if I have 10 of these tests? That 30 seconds turns into, like, I don&amp;rsquo;t know; math has never been my strong suit. But I bet it&amp;rsquo;s a lot.&lt;/p>
&lt;p>And that was an &lt;em>easy&lt;/em> example! Let&amp;rsquo;s look at something more complicated: here&amp;rsquo;s one of dozens of tests for the parser that that tokenizer feeds into:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="cp">#[test]&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">fn&lt;/span> &lt;span class="nf">test_nested_blocks&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">k9&lt;/span>::&lt;span class="n">snapshot&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">parse&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;
&lt;/span>&lt;span class="s">foo =
&lt;/span>&lt;span class="s"> x =
&lt;/span>&lt;span class="s"> y = 10
&lt;/span>&lt;span class="s"> z = 20
&lt;/span>&lt;span class="s"> y + z
&lt;/span>&lt;span class="s"> x
&lt;/span>&lt;span class="s">&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;foo (n) = (let ((x (let ((y 10) (z 20)) (+ y z)))) x)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Of course I could have typed out the exact syntax tree that I expected to see ahead of time, but I know that I won&amp;rsquo;t do that &amp;ndash; I don&amp;rsquo;t even want to time myself trying it. If I actually had to type the input &lt;em>and&lt;/em> output for every single test, I&amp;rsquo;d just write fewer of them.&lt;/p>
&lt;p>And yes, you still have to verify that the output is correct. But verification is almost always easier than constructing the output by hand, and in the rare cases where it isn&amp;rsquo;t, you still have the option to write down your expectations explicitly.&lt;/p>
&lt;p>And sometimes you don&amp;rsquo;t have to verify at all! Often when I&amp;rsquo;m very confident that my code already works correctly, but I&amp;rsquo;m about to make some risky changes to it, I&amp;rsquo;ll use this technique to generate a bunch of test cases, and just trust that the output is correct. Then after I&amp;rsquo;m done refactoring or optimizing, those tests will tell me if the new code ever diverges from my reference implementation.&lt;/p>
&lt;p>But I don&amp;rsquo;t just write more tests because it&amp;rsquo;s easy. I usually write more tests because it &lt;em>makes my job easier&lt;/em>. Which is the next beautiful thing that I want to highlight:&lt;/p>
&lt;h1 id="2-this-workflow-makes-tests-useful-immediately">2. This workflow makes tests useful &lt;em>immediately&lt;/em>&lt;/h1>
&lt;p>We all know that tests are valuable, but they&amp;rsquo;re not particularly valuable &lt;em>right now&lt;/em>. The point of automated testing isn&amp;rsquo;t to make sure that your code works, it&amp;rsquo;s to make sure that your code &lt;em>still&lt;/em> works in five years, right?&lt;/p>
&lt;p>But &lt;em>you&lt;/em> won&amp;rsquo;t be working on this same codebase in five years. And there&amp;rsquo;s no way that &lt;em>you&lt;/em> would make a change that breaks one of these subtle invariants you&amp;rsquo;ve come to rely on. The time you spend writing automated tests &lt;em>might&lt;/em> help &lt;em>someone&lt;/em>, eventually, but that&amp;rsquo;s time that you could instead be spending on &amp;ldquo;features&amp;rdquo; that your &amp;ldquo;company&amp;rdquo; needs to &amp;ldquo;make payroll this month.&amp;rdquo; Which means that &amp;ndash; if you&amp;rsquo;re already confident that your code is correct &amp;ndash; it can be tempting to not spend any time hardening it against the cold ravages of time.&lt;/p>
&lt;p>But when tests actually provide immediate value to &lt;em>you&lt;/em> &amp;ndash; when writing tests becomes a useful part of your workflow, when it becomes a part of &lt;em>how&lt;/em> you add new features &amp;ndash; suddenly the equation changes. You don&amp;rsquo;t write tests for some nebulous future benefit, you write tests because it&amp;rsquo;s the easiest way to run your code. The fact that this happens to make your codebase more robust to future changes is just icing.&lt;/p>
&lt;p>But in order to really benefit from this kind of testing, you might have to rethink what a &amp;ldquo;good test&amp;rdquo; looks like. Which brings me to&amp;hellip;&lt;/p>
&lt;h1 id="3-good-tests-are-good-observations-of-your-programs-behavior">3. Good tests are good observations of your program&amp;rsquo;s behavior&lt;/h1>
&lt;p>There are good tests, and there are bad tests. This is true whether or not you&amp;rsquo;re writing this particular style of test, but the ceiling for what a &amp;ldquo;good test&amp;rdquo; can look like is much, much higher when you&amp;rsquo;re using this technique.&lt;/p>
&lt;p>I&amp;rsquo;ll try to show you what I mean. Let&amp;rsquo;s look at a test for a function that finds the winner in a game of &lt;a href="https://en.wikipedia.org/wiki/Hex_(board_game)">Hex&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ocaml" data-lang="ocaml">&lt;span class="k">let&lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="n">expect_test&lt;/span> &lt;span class="s2">&amp;#34;find winning path&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span>
&lt;span class="k">let&lt;/span> &lt;span class="n">board&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">make_board&lt;/span> &lt;span class="o">~&lt;/span>&lt;span class="n">size&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="n">5&lt;/span> &lt;span class="s2">&amp;#34;A4 A2 B4 B3 C3 C4 D2 D3&amp;#34;&lt;/span> &lt;span class="k">in&lt;/span>
&lt;span class="n">print_s&lt;/span> &lt;span class="o">[%&lt;/span>&lt;span class="n">sexp&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">find_winning_path&lt;/span> &lt;span class="n">board&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="nn">Winning_path&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="o">)];&lt;/span>
&lt;span class="o">[%&lt;/span>&lt;span class="n">expect&lt;/span> &lt;span class="s2">&amp;#34;None&amp;#34;&lt;/span>&lt;span class="o">];&lt;/span>
&lt;span class="n">play_at&lt;/span> &lt;span class="n">board&lt;/span> &lt;span class="s2">&amp;#34;E2&amp;#34;&lt;/span>&lt;span class="o">;&lt;/span>
&lt;span class="n">print_s&lt;/span> &lt;span class="o">[%&lt;/span>&lt;span class="n">sexp&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">find_winning_path&lt;/span> &lt;span class="n">board&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="nn">Winning_path&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="o">)];&lt;/span>
&lt;span class="o">[%&lt;/span>&lt;span class="n">expect&lt;/span> &lt;span class="s2">&amp;#34;(Black (A4 B4 C3 D2 E2))&amp;#34;&lt;/span>&lt;span class="o">];&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Okay. Sure. I can sort of think for a minute and see that there was no winning path until black played at E2, and then there was one, and it&amp;rsquo;s probably reporting the correct path. But let&amp;rsquo;s compare that with an alternative:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ocaml" data-lang="ocaml">&lt;span class="k">let&lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="n">expect_test&lt;/span> &lt;span class="s2">&amp;#34;find winning path&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span>
&lt;span class="k">let&lt;/span> &lt;span class="n">board&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">make_board&lt;/span> &lt;span class="o">~&lt;/span>&lt;span class="n">size&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="n">5&lt;/span> &lt;span class="s2">&amp;#34;A4 A2 B4 B3 C3 C4 D2 D3&amp;#34;&lt;/span> &lt;span class="k">in&lt;/span>
&lt;span class="n">print_board&lt;/span> &lt;span class="n">board&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">find_winning_path&lt;/span> &lt;span class="n">board&lt;/span>&lt;span class="o">);&lt;/span>
&lt;span class="o">[%&lt;/span>&lt;span class="n">expect&lt;/span> &lt;span class="o">{|&lt;/span>
&lt;span class="nc">There&lt;/span> &lt;span class="n">is&lt;/span> &lt;span class="n">no&lt;/span> &lt;span class="n">winning&lt;/span> &lt;span class="n">path&lt;/span>&lt;span class="o">:&lt;/span>
&lt;span class="n">1&lt;/span> &lt;span class="n">2&lt;/span> &lt;span class="n">3&lt;/span> &lt;span class="n">4&lt;/span> &lt;span class="n">5&lt;/span>
&lt;span class="nn">A&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="n">w&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="n">b&lt;/span> &lt;span class="o">.&lt;/span>
&lt;span class="nn">B&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="n">w&lt;/span> &lt;span class="n">b&lt;/span> &lt;span class="o">.&lt;/span>
&lt;span class="nn">C&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="n">b&lt;/span> &lt;span class="n">w&lt;/span> &lt;span class="o">.&lt;/span>
&lt;span class="nn">D&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="n">b&lt;/span> &lt;span class="n">w&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="o">.&lt;/span>
&lt;span class="nn">E&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="p">.&lt;/span>
&lt;span class="o">|}];&lt;/span>
&lt;span class="n">play_at&lt;/span> &lt;span class="n">board&lt;/span> &lt;span class="s2">&amp;#34;E2&amp;#34;&lt;/span>&lt;span class="o">;&lt;/span>
&lt;span class="n">print_board&lt;/span> &lt;span class="n">board&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">find_winning_path&lt;/span> &lt;span class="n">board&lt;/span>&lt;span class="o">);&lt;/span>
&lt;span class="o">[%&lt;/span>&lt;span class="n">expect&lt;/span> &lt;span class="o">{|&lt;/span>
&lt;span class="nc">Black&lt;/span> &lt;span class="n">has&lt;/span> &lt;span class="n">a&lt;/span> &lt;span class="n">winning&lt;/span> &lt;span class="n">path&lt;/span>&lt;span class="o">:&lt;/span>
&lt;span class="n">1&lt;/span> &lt;span class="n">2&lt;/span> &lt;span class="n">3&lt;/span> &lt;span class="n">4&lt;/span> &lt;span class="n">5&lt;/span>
&lt;span class="nn">A&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="n">w&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="o">.&lt;/span>
&lt;span class="nn">B&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="n">w&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="o">.&lt;/span>
&lt;span class="nn">C&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">w&lt;/span> &lt;span class="o">.&lt;/span>
&lt;span class="nn">D&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">w&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="o">.&lt;/span>
&lt;span class="nn">E&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="o">.&lt;/span>
&lt;span class="o">|}];&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Isn&amp;rsquo;t that better? This test has convinced me in a single glance that the code is working correctly. And if, at some point in the future, the code &lt;em>stops&lt;/em> working correctly &amp;ndash; if I refactor the &lt;code>find_winning_path&lt;/code> function and break something &amp;ndash; I claim that this test will give me a decent head start on figuring out where I went wrong.&lt;/p>
&lt;p>Of course it took some work to write this visualization in the first place. Not a &lt;em>lot&lt;/em> of work &amp;ndash; it&amp;rsquo;s just a nested &lt;code>for&lt;/code> loop &amp;ndash; but still, it did take longer to write this test than the &lt;code>(Black (A4 B4 C3 D2 E2))&lt;/code> version.&lt;/p>
&lt;p>This is some of my favorite kind of work, though. It&amp;rsquo;s work that will speed up all of my future development, by making it easier for me to understand how my code works &amp;ndash; and, even better, by making it easier for &lt;em>other people&lt;/em> to understand how my code works. I only have to write the &lt;code>print_board&lt;/code> helper once, but I can use it in dozens of tests &amp;ndash; and so can my collaborators.&lt;/p>
&lt;p>Here&amp;rsquo;s another test that took a little work to produce, but that I found extremely useful during development:&lt;/p>
&lt;a class="image-container" href="https://ianthehenry.com/posts/my-kind-of-repl/configuration-spaces_huc30375726a9d7f8e19001b58c01707e4_90208_1138x1138_fit_box_3.png">&lt;picture>&lt;img
class=""
srcset="https://ianthehenry.com/posts/my-kind-of-repl/configuration-spaces_huc30375726a9d7f8e19001b58c01707e4_90208_569x569_fit_box_3.png 563w, https://ianthehenry.com/posts/my-kind-of-repl/configuration-spaces_huc30375726a9d7f8e19001b58c01707e4_90208_1138x1138_fit_box_3.png 1126w, https://ianthehenry.com/posts/my-kind-of-repl/configuration-spaces_huc30375726a9d7f8e19001b58c01707e4_90208_375x375_fit_box_3.png 371w, https://ianthehenry.com/posts/my-kind-of-repl/configuration-spaces_huc30375726a9d7f8e19001b58c01707e4_90208_750x750_fit_box_3.png 742w"
alt=""
title=""
sizes="(max-width: 400px) 375px, 569px"
width="563"
height="569"
style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAeElEQVR4nEyOQQvCUAyDk7R7uJsywbOI//&amp;#43;nid7c4UX6xnC5lJLmS/P&amp;#43;eNodNiUYIFpr63fl&amp;#43;bJ83i&amp;#43;SkliDy/UWEUlR0wQApFGZTTmOSoURvTuJQQHBMoTufyIyt4WS2Q&amp;#43;okAdiPLYn5tMcCpKjvpokAfgFAAD//4IFHFALN6wwAAAAAElFTkSuQmCC);"
/>&lt;/picture>&lt;/a>
&lt;p>You can imagine this as taking each of the top shapes and tracing them around the perimeter of the bottom shapes, maintaining contact at all times. Sort of a tricky function to write correctly, and one where visualization really helps me see that I got it right &amp;ndash; and, of course, to see what I did wrong when I inevitably break it.&lt;/p>
&lt;p>But that&amp;rsquo;s kind of cheating: that visualization uses Emacs&amp;rsquo;s &lt;code>iimage-mode&lt;/code> to render a literal image inline with my code, which is not something that you can do everywhere. And since part of the appeal of this technique is the universal accessibility, I should really stick to text.&lt;/p>
&lt;p>Here, here&amp;rsquo;s a better example. This is a function that, given a source of uniformly distributed random numbers, gives you a source of normally distributed random numbers:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-clojure" data-lang="clojure">&lt;span class="p">(&lt;/span>&lt;span class="kd">defn &lt;/span>&lt;span class="nv">marsaglia-sample&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nv">rng&lt;/span>&lt;span class="p">]&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">var &lt;/span>&lt;span class="nv">x&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">var &lt;/span>&lt;span class="nv">y&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">var &lt;/span>&lt;span class="nv">r2&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nf">while&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">or &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">= &lt;/span>&lt;span class="nv">r2&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">&amp;gt;= &lt;/span>&lt;span class="nv">r2&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nb">set &lt;/span>&lt;span class="nv">x&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">math/rng-uniform&lt;/span> &lt;span class="nv">rng&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nb">set &lt;/span>&lt;span class="nv">y&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">math/rng-uniform&lt;/span> &lt;span class="nv">rng&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nb">set &lt;/span>&lt;span class="nv">r2&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">+ &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">* &lt;/span>&lt;span class="nv">x&lt;/span> &lt;span class="nv">x&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">* &lt;/span>&lt;span class="nv">y&lt;/span> &lt;span class="nv">y&lt;/span>&lt;span class="p">))))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">def &lt;/span>&lt;span class="nv">mag&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">math/sqrt&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">/ &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">* &lt;/span>&lt;span class="mi">-2&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">math/log&lt;/span> &lt;span class="nv">r2&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="nv">r2&lt;/span>&lt;span class="p">)))&lt;/span>
&lt;span class="p">[(&lt;/span>&lt;span class="nb">* &lt;/span>&lt;span class="nv">x&lt;/span> &lt;span class="nv">mag&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">* &lt;/span>&lt;span class="nv">y&lt;/span> &lt;span class="nv">mag&lt;/span>&lt;span class="p">)])&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Except&amp;hellip; does it work? I just &lt;a href="https://en.wikipedia.org/wiki/Marsaglia_polar_method">copied this from Wikipedia&lt;/a>; I don&amp;rsquo;t really have very good intuition for the algorithm yet. Did I implement it correctly?&lt;/p>
&lt;p>It&amp;rsquo;s not immediately obvious how I would write a test for this. But if I &lt;em>weren&amp;rsquo;t&lt;/em> trying to &amp;ldquo;test&amp;rdquo; it &amp;ndash; if I just wanted to convince myself that it worked &amp;ndash; then I&amp;rsquo;d probably plot a few thousand points from the distribution and check if they &lt;em>look&lt;/em> normally distributed. And if I had any doubts after that, maybe I&amp;rsquo;d calculate the mean and standard deviation as well and check that they&amp;rsquo;re close to the values I expect.&lt;/p>
&lt;p>So that&amp;rsquo;s exactly what I want my automated tests to do:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-clojure" data-lang="clojure">&lt;span class="p">(&lt;/span>&lt;span class="nf">deftest&lt;/span> &lt;span class="s">&amp;#34;marsaglia sample produces a normal distribution&amp;#34;&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">def &lt;/span>&lt;span class="nv">rng&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">math/rng&lt;/span> &lt;span class="mi">1234&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nf">test-stdout&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">plot-histogram&lt;/span> &lt;span class="err">|&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">marsaglia-sample&lt;/span> &lt;span class="nv">rng&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="mi">100000&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">`&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣷⣾⣦⣆⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣷⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⣷⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣦⣄⡀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣤⣤⣄&lt;/span>
&lt;span class="nv">μ&lt;/span> &lt;span class="nb">= &lt;/span>&lt;span class="mf">0.796406&lt;/span>
&lt;span class="nv">σ&lt;/span> &lt;span class="nb">= &lt;/span>&lt;span class="mf">0.601565&lt;/span>
&lt;span class="o">`&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>And hark! That&amp;rsquo;s clearly wrong. And the test tells me exactly &lt;em>how&lt;/em> it&amp;rsquo;s wrong: it&amp;rsquo;s only producing positive numbers (the histogram is centered around the mean). This helps me pinpoint my mistake, and to fix it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-clojure" data-lang="clojure">&lt;span class="p">(&lt;/span>&lt;span class="kd">defn &lt;/span>&lt;span class="nv">symmetric-random&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nv">rng&lt;/span> &lt;span class="nv">x&lt;/span>&lt;span class="p">]&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nb">- &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">* &lt;/span>&lt;span class="mi">2&lt;/span> &lt;span class="nv">x&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">math/rng-uniform&lt;/span> &lt;span class="nv">rng&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="nv">x&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="kd">defn &lt;/span>&lt;span class="nv">marsaglia-sample&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nv">rng&lt;/span>&lt;span class="p">]&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">var &lt;/span>&lt;span class="nv">x&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">var &lt;/span>&lt;span class="nv">y&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">var &lt;/span>&lt;span class="nv">r2&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nf">while&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">or &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">= &lt;/span>&lt;span class="nv">r2&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">&amp;gt;= &lt;/span>&lt;span class="nv">r2&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nb">set &lt;/span>&lt;span class="nv">x&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">symmetric-random&lt;/span> &lt;span class="nv">rng&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nb">set &lt;/span>&lt;span class="nv">y&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">symmetric-random&lt;/span> &lt;span class="nv">rng&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nb">set &lt;/span>&lt;span class="nv">r2&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">+ &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">* &lt;/span>&lt;span class="nv">x&lt;/span> &lt;span class="nv">x&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">* &lt;/span>&lt;span class="nv">y&lt;/span> &lt;span class="nv">y&lt;/span>&lt;span class="p">))))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">def &lt;/span>&lt;span class="nv">mag&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">math/sqrt&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">/ &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">* &lt;/span>&lt;span class="mi">-2&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">math/log&lt;/span> &lt;span class="nv">r2&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="nv">r2&lt;/span>&lt;span class="p">)))&lt;/span>
&lt;span class="p">[(&lt;/span>&lt;span class="nb">* &lt;/span>&lt;span class="nv">x&lt;/span> &lt;span class="nv">mag&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">* &lt;/span>&lt;span class="nv">y&lt;/span> &lt;span class="nv">mag&lt;/span>&lt;span class="p">)])&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nf">deftest&lt;/span> &lt;span class="s">&amp;#34;marsaglia sample produces a normal distribution&amp;#34;&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">def &lt;/span>&lt;span class="nv">rng&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">math/rng&lt;/span> &lt;span class="mi">1234&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nf">test-stdout&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">plot-histogram&lt;/span> &lt;span class="err">|&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">marsaglia-sample&lt;/span> &lt;span class="nv">rng&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="mi">100000&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">`&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣦⣾⣿⣦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⣿⣿⣿⣆⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⡀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⢀⣀⣀⣤⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣤⣀⣀⡀&lt;/span>
&lt;span class="nv">μ&lt;/span> &lt;span class="nb">= &lt;/span>&lt;span class="mf">0.001965&lt;/span>
&lt;span class="nv">σ&lt;/span> &lt;span class="nb">= &lt;/span>&lt;span class="mf">0.998886&lt;/span>
&lt;span class="o">`&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>And, like, yes: writing this test required me to write a Unicode histogram plotter thingy, like a crazy person. But I&amp;rsquo;ve &lt;em>already written&lt;/em> a Unicode histogram plotter thingy, like a crazy person, so I was able to re-use it here &amp;ndash; just as I will be able to re-use it again in the future, the next time I feel like it will be useful.&lt;/p>
&lt;p>In fact I&amp;rsquo;ve accumulated a lot of little visualization helpers like this over the years &amp;ndash; helpers that allow me to better observe the behavior of my code. These helpers are useful when I&amp;rsquo;m writing automated tests, because they help me understand when the behavior of my code changes, but they&amp;rsquo;re also useful for regular ad-hoc development. It&amp;rsquo;s useful to be able to whip up a histogram, or a nicely formatted table,&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup> or a graph of a function, when I&amp;rsquo;m doing regular old printf debugging.&lt;/p>
&lt;p>Which reminds me&amp;hellip;&lt;/p>
&lt;h1 id="4-you-can-use-printf-debugging-right-in-your-tests">4. You can use printf debugging right in your tests&lt;/h1>
&lt;p>I&amp;rsquo;ve shown you two types of these &amp;ldquo;REPL tests&amp;rdquo; so far.&lt;/p>
&lt;p>There&amp;rsquo;s a simple expression-oriented API:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-clojure" data-lang="clojure">&lt;span class="p">(&lt;/span>&lt;span class="nb">test &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">+ &lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>And there&amp;rsquo;s the imperative stdout-based API:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ocaml" data-lang="ocaml">&lt;span class="k">let&lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="n">expect_test&lt;/span> &lt;span class="s2">&amp;#34;board visualization&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span>
&lt;span class="n">print_board&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">make_board&lt;/span> &lt;span class="o">~&lt;/span>&lt;span class="n">size&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="n">5&lt;/span> &lt;span class="s2">&amp;#34;A4 A2 B4 B3 C3 C4 D2 D3&amp;#34;&lt;/span>&lt;span class="o">);&lt;/span>
&lt;span class="o">[%&lt;/span>&lt;span class="n">expect&lt;/span> &lt;span class="o">{|&lt;/span>
&lt;span class="n">1&lt;/span> &lt;span class="n">2&lt;/span> &lt;span class="n">3&lt;/span> &lt;span class="n">4&lt;/span> &lt;span class="n">5&lt;/span>
&lt;span class="nn">A&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="n">w&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="n">b&lt;/span> &lt;span class="o">.&lt;/span>
&lt;span class="nn">B&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="n">w&lt;/span> &lt;span class="n">b&lt;/span> &lt;span class="o">.&lt;/span>
&lt;span class="nn">C&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="n">b&lt;/span> &lt;span class="n">w&lt;/span> &lt;span class="o">.&lt;/span>
&lt;span class="nn">D&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="n">b&lt;/span> &lt;span class="n">w&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="o">.&lt;/span>
&lt;span class="nn">E&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="p">.&lt;/span>
&lt;span class="o">|}];&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>When I first starting writing tests like this, the stdout-based API seemed a little weird to me. Most of my tests just printed simple little expressions, and the fact that I had to spread that out across multiple lines was annoying. And if I did want to create a more interesting visualization, like the one above, couldn&amp;rsquo;t I just write an expression to return a formatted string?&lt;/p>
&lt;p>Yes, but: &lt;code>print&lt;/code> is quick, easy, and familiar. It&amp;rsquo;s &lt;em>nice&lt;/em> that rendering that board was just a nested for loop where I printed out one character at a time. Of course I could have allocated some kind of string builder, and appended characters to that, but that&amp;rsquo;s a &lt;em>little&lt;/em> bit more friction than just calling &lt;code>print&lt;/code>. I&amp;rsquo;d rather let the test framework do it for me automatically: the easier it is to write visualizations like this, the more likely I am to do it.&lt;/p>
&lt;p>But when I actually spent time writing tests using this API, I came to really appreciate a subtle point that I had initially overlooked: stdout is dynamically scoped.&lt;/p>
&lt;p>Which means that if I call &lt;code>print&lt;/code> in a helper of a helper of a helper, it&amp;rsquo;s going to appear in the output of my test. I don&amp;rsquo;t have to thread a string builder through the call stack; I can just &lt;code>printf&lt;/code>, and get &lt;code>printf&lt;/code>-debugging right in my tests.&lt;/p>
&lt;p>For example: our function to make normally distributed random numbers relies on &amp;ldquo;rejection sampling&amp;rdquo; &amp;ndash; we have to generate uniformly distributed random numbers until we find a pair that lies inside the unit circle.&lt;/p>
&lt;p>I&amp;rsquo;m curious how often we actually have to re-roll the dice. It&amp;rsquo;s not an observable property of our function, but we can just print it out in the implementation:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-clojure" data-lang="clojure">&lt;span class="p">(&lt;/span>&lt;span class="kd">defn &lt;/span>&lt;span class="nv">marsaglia-sample&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nv">rng&lt;/span>&lt;span class="p">]&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">var &lt;/span>&lt;span class="nv">x&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">var &lt;/span>&lt;span class="nv">y&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">var &lt;/span>&lt;span class="nv">r2&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">var &lt;/span>&lt;span class="nv">iterations&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nf">while&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">or &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">= &lt;/span>&lt;span class="nv">r2&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">&amp;gt;= &lt;/span>&lt;span class="nv">r2&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nb">set &lt;/span>&lt;span class="nv">x&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">symmetric-random&lt;/span> &lt;span class="nv">rng&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nb">set &lt;/span>&lt;span class="nv">y&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">symmetric-random&lt;/span> &lt;span class="nv">rng&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nb">set &lt;/span>&lt;span class="nv">r2&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">+ &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">* &lt;/span>&lt;span class="nv">x&lt;/span> &lt;span class="nv">x&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">* &lt;/span>&lt;span class="nv">y&lt;/span> &lt;span class="nv">y&lt;/span>&lt;span class="p">)))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nf">++&lt;/span> &lt;span class="nv">iterations&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">def &lt;/span>&lt;span class="nv">rejections&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">- &lt;/span>&lt;span class="nv">iterations&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nb">when &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">&amp;gt; &lt;/span>&lt;span class="nv">rejections&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nf">printf&lt;/span> &lt;span class="s">&amp;#34;rejected %d values&amp;#34;&lt;/span> &lt;span class="nv">rejections&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">def &lt;/span>&lt;span class="nv">mag&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">math/sqrt&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">/ &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">* &lt;/span>&lt;span class="mi">-2&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">math/log&lt;/span> &lt;span class="nv">r2&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="nv">r2&lt;/span>&lt;span class="p">)))&lt;/span>
&lt;span class="p">[(&lt;/span>&lt;span class="nb">* &lt;/span>&lt;span class="nv">x&lt;/span> &lt;span class="nv">mag&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">* &lt;/span>&lt;span class="nv">y&lt;/span> &lt;span class="nv">mag&lt;/span>&lt;span class="p">)])&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>And then re-run our test, exactly like we did before:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-clojure" data-lang="clojure">&lt;span class="p">(&lt;/span>&lt;span class="nf">deftest&lt;/span> &lt;span class="s">&amp;#34;marsaglia sample produces a normal distribution&amp;#34;&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">def &lt;/span>&lt;span class="nv">rng&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">math/rng&lt;/span> &lt;span class="mi">1234&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nf">test-stdout&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">plot-histogram&lt;/span> &lt;span class="err">|&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">marsaglia-sample&lt;/span> &lt;span class="nv">rng&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="mi">100&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">`&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="nv">rejected&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="nv">values&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⡇⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⡇⠀⡀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⡇⠀⡇⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡄⢠⠀⢸⣤⡄⡇⢠⣧⡇⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⢠⡇⢸⡄⣼⣿⡇⡇⢸⣿⡇⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡆⡆⠀⢸⢸⡇⢸⡇⣿⣿⡇⡇⣾⣿⡇⡇⠀⠀⠀⢰⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⠀⠀⠀⡆⠀⠀⠀⡇⣷⣶⢸⣾⣷⣾⣷⣿⣿⣷⣷⣿⣿⣷⡇⢰⡆⠀⣾⡇⡆⠀⠀⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⢰⠀⠀⡇⠀⡆⡆⣷⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⢸⣷⡆⣿⡇⣷⢰⡆⠀⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="err">⠀⠀⠀⢸⢸⡇⡇⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⣿⣿⣿⡇⣿⠀⠀⠀⠀⠀&lt;/span>
&lt;span class="nv">μ&lt;/span> &lt;span class="nb">= &lt;/span>&lt;span class="mf">-0.054361&lt;/span>
&lt;span class="nv">σ&lt;/span> &lt;span class="nb">= &lt;/span>&lt;span class="mf">1.088199&lt;/span>
&lt;span class="o">`&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Alright, so we rejected 33 points, which means that we generated 133 &amp;ndash; a rejection rate of around 25%. The area of the unit circle is π, and the area of its bounding square is 4, so I&amp;rsquo;d expect us to reject around (1 - π/4) ≈ 21.5% of points. Pretty close!&lt;/p>
&lt;p>Now, this is the sort of thing that I wouldn&amp;rsquo;t keep around in an automated test. This was exploratory; this was a gut check. That output is not in a very useful format, and I don&amp;rsquo;t think it&amp;rsquo;s really worth ensuring that this rate remains stable over time &amp;ndash; I can&amp;rsquo;t imagine that I&amp;rsquo;ll screw that up without breaking the function entirely. But the important takeaway is that it was really, really easy to sprinkle &lt;code>printf&lt;/code>s into code, and to see their output right next to the code itself, using the exact same workflow that I&amp;rsquo;m already using to test my code.&lt;/p>
&lt;p>And while this output was temporary, I actually do wind up committing many of the exploratory expressions that I run as I&amp;rsquo;m developing. Because&amp;hellip;&lt;/p>
&lt;h1 id="5-good-tests-make-good-documentation">5. Good tests make good documentation&lt;/h1>
&lt;p>Compare:&lt;/p>
&lt;blockquote>
&lt;p>&lt;code>sep-when&lt;/code> is a function that takes a list and a predicate, and breaks the input list into a list of sub-lists every time the predicate returns true for an element.&lt;/p>
&lt;/blockquote>
&lt;p>With:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-clojure" data-lang="clojure">&lt;span class="p">(&lt;/span>&lt;span class="nb">test &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">sep-when&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="mi">3&lt;/span> &lt;span class="mi">5&lt;/span> &lt;span class="mi">3&lt;/span> &lt;span class="mi">6&lt;/span> &lt;span class="mi">7&lt;/span> &lt;span class="mi">8&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="nv">even?&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">[[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">2&lt;/span> &lt;span class="mi">3&lt;/span> &lt;span class="mi">5&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">6&lt;/span> &lt;span class="mi">7&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">8&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">]])&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>My brain can intuitively understand the behavior of this function from the example code more easily than it can parse and understand the (ambiguous!) English description. The description is still helpful, sure, but the example conveys &lt;em>more&lt;/em> information &lt;em>more quickly&lt;/em>. To me, anyway.&lt;/p>
&lt;p>I think that this is mostly because my brain is very comfortable reading REPL sessions, which might not be true for everyone. And I&amp;rsquo;m not saying that good English-language documentation isn&amp;rsquo;t important &amp;ndash; ideally, of course, you have both. But there are two important advantages to the second version:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>Tests don&amp;rsquo;t lie; they can&amp;rsquo;t become stale.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>I didn&amp;rsquo;t have to write it.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>I spent a couple minutes trying to come up with a pithy English description of this function, and I don&amp;rsquo;t even think I did a very good job. Writing good documentation is hard, but generating examples is easy &amp;ndash; especially when my test framework is doing half the work.&lt;/p>
&lt;p>And since it&amp;rsquo;s easy to generate examples, I do it in cases where I wouldn&amp;rsquo;t have bothered to write documentation in the first place. This &lt;code>sep-when&lt;/code> example was a real actual helper function that I pulled from a random side project. A private, trivial helper, not part of any public API, that I just happened to write as a standalone function &amp;ndash; in other words, not the sort of function that I would ever think to document.&lt;/p>
&lt;p>It&amp;rsquo;s also a trivial function, far from the actual domain of the problem I was working on, so it&amp;rsquo;s not the sort of thing I would usually bother to &lt;em>test&lt;/em> either. And I might regret that &amp;ndash; bugs in &amp;ldquo;trivial&amp;rdquo; helpers can turn into bugs in actual domain logic that can be difficult to track down, and I probably only have a 50/50 shot of getting a trivial function right on the first try in a dynamically-typed language.&lt;/p>
&lt;p>But when the ergonomics of writing tests is this nice, I actually &lt;em>do&lt;/em> bother to test trivial helpers like this. Testing just means writing down an expression to try in my &amp;ldquo;REPL,&amp;rdquo; and I can do that right next to the implementation of the function itself:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-clojure" data-lang="clojure">&lt;span class="p">(&lt;/span>&lt;span class="kd">defn &lt;/span>&lt;span class="nv">sep-when&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nb">list &lt;/span>&lt;span class="nv">f&lt;/span>&lt;span class="p">]&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">var &lt;/span>&lt;span class="nv">current-chunk&lt;/span> &lt;span class="nv">nil&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="k">def &lt;/span>&lt;span class="nv">chunks&lt;/span> &lt;span class="o">@&lt;/span>&lt;span class="p">[])&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nf">eachp&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nv">i&lt;/span> &lt;span class="nv">el&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="nv">list&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nb">when &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">or &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">= &lt;/span>&lt;span class="nv">i&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">f&lt;/span> &lt;span class="nv">el&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nb">set &lt;/span>&lt;span class="nv">current-chunk&lt;/span> &lt;span class="o">@&lt;/span>&lt;span class="p">[])&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nf">array/push&lt;/span> &lt;span class="nv">chunks&lt;/span> &lt;span class="nv">current-chunk&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nf">array/push&lt;/span> &lt;span class="nv">current-chunk&lt;/span> &lt;span class="nv">el&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="nv">chunks&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nb">test &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">sep-when&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="mi">3&lt;/span> &lt;span class="mi">5&lt;/span> &lt;span class="mi">3&lt;/span> &lt;span class="mi">6&lt;/span> &lt;span class="mi">7&lt;/span> &lt;span class="mi">8&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="nv">even?&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">[[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">2&lt;/span> &lt;span class="mi">3&lt;/span> &lt;span class="mi">5&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">6&lt;/span> &lt;span class="mi">7&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">8&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">]])&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Which means that, not only will I catch dumb mistakes early, but I also automatically generate examples for myself as I code. If I come back to this code a year from now and can&amp;rsquo;t remember what this function was supposed to do, I only have to glance at the test to remind myself.&lt;/p>
&lt;h1 id="and-on-and-on">And on, and on&amp;hellip;&lt;/h1>
&lt;p>I could keep going, but I think that my returns are starting to diminish here. If you still aren&amp;rsquo;t convinced, &lt;a href="https://jsomers.net/">James Somers&lt;/a> wrote &lt;a href="https://blog.janestreet.com/the-joy-of-expect-tests/">an excellent article about this workflow&lt;/a> that I would recommend if you want to see more examples of what good tests can look like in a production setting.&lt;/p>
&lt;p>But I think that it&amp;rsquo;s difficult to convey just how good this workflow actually is in any piece of writing. I think that it&amp;rsquo;s something you have to try for yourself.&lt;/p>
&lt;p>And you probably can! This technique is usually called &amp;ldquo;inline snapshot testing,&amp;rdquo; or sometimes &amp;ldquo;golden master testing,&amp;rdquo; and if you google those phrases you will probably find a library that lets you try it out in your language of choice. I have personally only used this technique in OCaml, Janet, and Rust, but in the spirit of inclusivity, I also managed to get this working working in JavaScript, in VSCode, using a test framework called &lt;a href="https://jestjs.io/docs/snapshot-testing#inline-snapshots">Jest&lt;/a>:&lt;/p>
&lt;div class="video-container">
&lt;video controls
width="877"
height="475"
preload=metadata
poster="/posts/my-kind-of-repl/jest-poster_hud019e4dd1adeea0d3cd2d8ff08f28831_227756_1754x950_fit_q75_h2_box_3.936adefc66dc8239d1d193bc902d1c7231115b991b8bb077f0adebd5febc3ad6.webp"
style="
background-size: cover;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAARCAIAAAAzPjmrAAAB10lEQVR4nLSUW24VPQzHbceTyVzO96kt8MwK&amp;#43;gQsg32wOZ7ZAdvgBSEBLe10rrnYKFOEjujpqOgcfk&amp;#43;jSez/37ETfvvmtXkmnzzQD3iMpmmcc8aYEEJKCQCYeVmWYRgebj47O4sxElGMUUTw8vKSiO6629KWtqqJKKWkqgrQdx0R/o5c//0CER9zc7&amp;#43;tLEsRSSkxM9&amp;#43;&amp;#43;eOWvPiiEoNPqrhAR7xcfwr7AX4L3RnOl9eePwbIjVS6csyIJiRWiKosKZlOgABKAiLIvTdu52VBIEkJ0ZZkrUFUk6oP44bptmrau4jS1KKmyWlr1cxJFRa0qIV6CTP33bYGYBJIAQNs2nFISkTh5JAIAH1MfIAlrmJR4WKJ1/&amp;#43;VqQBAJQbkArv&amp;#43;P4&amp;#43;0TT4qttSJZjVaBtuSbLs/TsloBgDCPD8OIzEZSVblvNZHheZ7317pxMWYr&amp;#43;CnEqJC7BiKJcSUfjvfrWjgy&amp;#43;x/kDu8P&amp;#43;Mmhf5f6xALW2sMC1lrn3PECzHxY4FQNGMcD0zzPC208W8czjCNtP43Hgxfv3te7HRv8dheft&amp;#43;Z6FFfgrqQpSP5mvKgJERDpax/7RXcWXEGW8WpIL8&amp;#43;Lu0VuJrmoafAKqp3X88o0Nt&amp;#43;tL10sZfoZAAD//4IFDj8c6dvsAAAAAElFTkSuQmCC);
">
&lt;source src="https://ianthehenry.com/posts/my-kind-of-repl/jest-1754x950.8a767fd291f5f664d68306606066827dc2609325127e8d371408c068864cc262.mp4" type="video/mp4">
&lt;/video>
&lt;/div>
&lt;p>I will be the first to admit that that workflow is not ideal; Jest is absolutely not designed for this &amp;ldquo;read-eval-patch-loop&amp;rdquo; style of development. Inline snapshots are a bolted-on extra feature on top of a traditional &amp;ldquo;fluent&amp;rdquo; assertion-based testing API, and it might take some work to twist Jest into something useful if you actually wanted to adopt this workflow in JavaScript. But it&amp;rsquo;s possible! The hard part is done at least; the source rewriter is written. The rest is just ergonomics.&lt;/p>
&lt;p>There is a cheat code, though, if you cannot find a test framework that works in your language: &lt;a href="https://bitheap.org/cram/">Cram&lt;/a>.&lt;/p>
&lt;p>Cram is basically &amp;ldquo;inline snapshot tests for bash,&amp;rdquo; but since you can write a bash command to run a program written in any other language, you can use Cram as a language-agnostic snapshot testing tool. It isn&amp;rsquo;t as nice as a native testing framework, but it&amp;rsquo;s still far, far nicer than writing assertion-based tests. And it works anywhere &amp;ndash; I use Cram to &lt;a href="https://github.com/ianthehenry/judge/blob/master/tests/basic.t">test the test framework&lt;/a> that I wrote in Janet, for example, since I can&amp;rsquo;t exactly use it to test itself.&lt;/p>
&lt;p>Okay. I really am almost done, but I feel that I should say one last thing about this type of testing, in case you are still hesitant: this is a &lt;em>mechanic&lt;/em>. This is not a philosophy or a dogma. This is just an ergonomic way to write tests &amp;ndash; &lt;em>any&lt;/em> tests.&lt;/p>
&lt;p>You can still argue as much as you want about unit tests vs. integration tests and mocks vs. stubs. You can test for narrow, individually significant properties, or just broadly observe entire data structures. You can still debate the merits of code coverage, and you can even combine this technique with quickcheck-style automatic property tests. Here, look:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ocaml" data-lang="ocaml">&lt;span class="k">module&lt;/span> &lt;span class="nc">Vec2&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">struct&lt;/span>
&lt;span class="k">type&lt;/span> &lt;span class="n">t&lt;/span> &lt;span class="o">=&lt;/span>
&lt;span class="o">{&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="kt">float&lt;/span>
&lt;span class="o">;&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="kt">float&lt;/span>
&lt;span class="o">}&lt;/span>
&lt;span class="o">[@@&lt;/span>&lt;span class="n">deriving&lt;/span> &lt;span class="n">quickcheck&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">sexp_of&lt;/span>&lt;span class="o">]&lt;/span>
&lt;span class="k">let&lt;/span> &lt;span class="n">length&lt;/span> &lt;span class="o">{&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="o">;&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">}&lt;/span> &lt;span class="o">=&lt;/span>
&lt;span class="n">sqrt&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="o">*.&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">+.&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">*.&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="o">)&lt;/span>
&lt;span class="o">;;&lt;/span>
&lt;span class="k">let&lt;/span> &lt;span class="n">normalize&lt;/span> &lt;span class="n">point&lt;/span> &lt;span class="o">=&lt;/span>
&lt;span class="k">let&lt;/span> &lt;span class="n">length&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">length&lt;/span> &lt;span class="n">point&lt;/span> &lt;span class="k">in&lt;/span>
&lt;span class="o">{&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">point&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="o">/.&lt;/span> &lt;span class="n">length&lt;/span>
&lt;span class="o">;&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">point&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">y&lt;/span> &lt;span class="o">/.&lt;/span> &lt;span class="n">length&lt;/span>
&lt;span class="o">}&lt;/span>
&lt;span class="o">;;&lt;/span>
&lt;span class="k">end&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Pretty simple code, but does it work for all inputs? Well, let&amp;rsquo;s try a property test:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ocaml" data-lang="ocaml">&lt;span class="k">let&lt;/span> &lt;span class="n">assert_nearly_equal&lt;/span> &lt;span class="n">here&lt;/span> &lt;span class="n">actual&lt;/span> &lt;span class="n">expected&lt;/span> &lt;span class="o">~&lt;/span>&lt;span class="n">epsilon&lt;/span> &lt;span class="o">=&lt;/span>
&lt;span class="o">[%&lt;/span>&lt;span class="n">test_pred&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kt">float&lt;/span>&lt;span class="o">]&lt;/span>
&lt;span class="o">~&lt;/span>&lt;span class="n">here&lt;/span>&lt;span class="o">:[&lt;/span>&lt;span class="n">here&lt;/span>&lt;span class="o">]&lt;/span>
&lt;span class="o">(&lt;/span>&lt;span class="k">fun&lt;/span> &lt;span class="n">actual&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nn">Float&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="o">(&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="o">)&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="nn">Float&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">abs&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">actual&lt;/span> &lt;span class="o">-.&lt;/span> &lt;span class="n">expected&lt;/span>&lt;span class="o">))&lt;/span> &lt;span class="n">epsilon&lt;/span>&lt;span class="o">)&lt;/span>
&lt;span class="n">actual&lt;/span>
&lt;span class="k">let&lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="n">expect_test&lt;/span> &lt;span class="s2">&amp;#34;normalize returns a vector of length 1&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span>
&lt;span class="nn">Quickcheck&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">test&lt;/span> &lt;span class="o">[%&lt;/span>&lt;span class="n">quickcheck&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">generator&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nn">Vec2&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="o">]&lt;/span>
&lt;span class="o">~&lt;/span>&lt;span class="n">sexp_of&lt;/span>&lt;span class="o">:[%&lt;/span>&lt;span class="n">sexp_of&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nn">Vec2&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="o">]&lt;/span>
&lt;span class="o">~&lt;/span>&lt;span class="n">f&lt;/span>&lt;span class="o">:(&lt;/span>&lt;span class="k">fun&lt;/span> &lt;span class="n">point&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span>
&lt;span class="n">assert_nearly_equal&lt;/span> &lt;span class="o">[%&lt;/span>&lt;span class="n">here&lt;/span>&lt;span class="o">]&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="nn">Vec2&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">length&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="nn">Vec2&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">normalize&lt;/span> &lt;span class="n">point&lt;/span>&lt;span class="o">))&lt;/span> &lt;span class="n">1&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">0&lt;/span> &lt;span class="o">~&lt;/span>&lt;span class="n">epsilon&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="n">0&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">0001&lt;/span>&lt;span class="o">)&lt;/span>
&lt;span class="o">[@@&lt;/span>&lt;span class="n">expect&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">uncaught_exn&lt;/span> &lt;span class="o">{|&lt;/span>
&lt;span class="c">(* CR expect_test_collector: This test expectation appears to contain a backtrace.
&lt;/span>&lt;span class="c"> This is strongly discouraged as backtraces are fragile.
&lt;/span>&lt;span class="c"> Please change this test to not include a backtrace. *)&lt;/span>
&lt;span class="o">(&lt;/span>&lt;span class="s2">&amp;#34;Base_quickcheck.Test.run: test failed&amp;#34;&lt;/span>
&lt;span class="o">(&lt;/span>&lt;span class="n">input&lt;/span> &lt;span class="o">((&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">3&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">3810849992682576E&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="n">272&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">y&lt;/span> &lt;span class="n">440&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">10383674821787&lt;/span>&lt;span class="o">)))&lt;/span>
&lt;span class="o">(&lt;/span>&lt;span class="n">error&lt;/span>
&lt;span class="o">((&lt;/span>&lt;span class="n">runtime&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">lib&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">runtime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ml&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="nc">E&lt;/span> &lt;span class="s2">&amp;#34;predicate failed&amp;#34;&lt;/span>
&lt;span class="o">((&lt;/span>&lt;span class="nc">Value&lt;/span> &lt;span class="n">0&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="nc">Loc&lt;/span> &lt;span class="n">test&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">demo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ml&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="n">20&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="n">15&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="nc">Stack&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">test&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">demo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ml&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="n">29&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="n">26&lt;/span>&lt;span class="o">))))&lt;/span>
&lt;span class="s2">&amp;#34;Raised at Ppx_assert_lib__Runtime.test_pred in file &lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="s2">runtime-lib/runtime.ml&lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="s2">, line 58, characters 4-58\
&lt;/span>&lt;span class="s2"> &lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">Called from Base__Or_error.try_with in file &lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="s2">src/or_error.ml&lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="s2">, line 84, characters 9-15\
&lt;/span>&lt;span class="s2"> &lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="o">)))&lt;/span>
&lt;span class="nc">Raised&lt;/span> &lt;span class="n">at&lt;/span> &lt;span class="nn">Base__Error&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">raise&lt;/span> &lt;span class="k">in&lt;/span> &lt;span class="n">file&lt;/span> &lt;span class="s2">&amp;#34;src/error.ml&amp;#34;&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">inlined&lt;/span>&lt;span class="o">),&lt;/span> &lt;span class="n">line&lt;/span> &lt;span class="n">9&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">characters&lt;/span> &lt;span class="n">14&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">30&lt;/span>
&lt;span class="nc">Called&lt;/span> &lt;span class="n">from&lt;/span> &lt;span class="nn">Base__Or_error&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ok_exn&lt;/span> &lt;span class="k">in&lt;/span> &lt;span class="n">file&lt;/span> &lt;span class="s2">&amp;#34;src/or_error.ml&amp;#34;&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">line&lt;/span> &lt;span class="n">92&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">characters&lt;/span> &lt;span class="n">17&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">32&lt;/span>
&lt;span class="nc">Called&lt;/span> &lt;span class="n">from&lt;/span> &lt;span class="nn">Expect_test_collector&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nn">Make&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nn">Instance_io&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">exec&lt;/span> &lt;span class="k">in&lt;/span> &lt;span class="n">file&lt;/span> &lt;span class="s2">&amp;#34;collector/expect_test_collector.ml&amp;#34;&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">line&lt;/span> &lt;span class="n">262&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">characters&lt;/span> &lt;span class="n">12&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">19&lt;/span> &lt;span class="o">|}]&lt;/span>
&lt;span class="o">;;&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is not, you know, a &lt;em>good&lt;/em> property test, but just focus on the workflow &amp;ndash; even though it&amp;rsquo;s just an assertion failure, it&amp;rsquo;s nice to see that error in context. I don&amp;rsquo;t need to manually correlate line numbers with my source files, even if I&amp;rsquo;m working in a text editor that can&amp;rsquo;t jump to errors automatically.&lt;/p>
&lt;p>So the mechanic of source patching is still somewhat useful regardless of what sort of tests you&amp;rsquo;re writing. This post has focused on pretty simple unit tests because, you know, it&amp;rsquo;s a blog post, and we just don&amp;rsquo;t have time to page in anything complicated together. And I&amp;rsquo;ve especially emphasized testing-as-observation because that happens to be my favorite way to write &lt;em>most&lt;/em> tests, and it&amp;rsquo;s a unique superpower that you can&amp;rsquo;t really replicate in a traditional test framework.&lt;/p>
&lt;p>But that doesn&amp;rsquo;t mean that read-eval-patch loops are only useful in these cases, or that you can only adopt this technique with a radical change to the way you think about testing.&lt;/p>
&lt;p>Although it might not hurt&amp;hellip;&lt;/p>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>The convention where I work is to render all data structures as s-expressions, but I didn&amp;rsquo;t want to upset you any further.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2" role="doc-endnote">
&lt;p>Rust also has &lt;a href="https://docs.rs/expect-test/latest/expect_test/">&lt;code>expect_test&lt;/code>&lt;/a>, which I have not tried, and &lt;a href="https://insta.rs/">Insta&lt;/a>, but I had trouble getting it to work when I tried it a few years ago.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3" role="doc-endnote">
&lt;p>I couldn&amp;rsquo;t find room for this in the post &amp;ndash; it isn&amp;rsquo;t very flashy &amp;ndash; but in real life I think that tables are the most useful tool in my testing lunchbox. You can see &lt;a href="https://blog.janestreet.com/computations-that-differentiate-debug-and-document-themselves/">a few examples here&lt;/a> of tests with tables in them, although the tables are not really the stars there.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><pubDate>Wed, 05 Jul 2023 00:00:00 +0000</pubDate><link>https://ianthehenry.com/posts/my-kind-of-repl/</link><guid isPermaLink="true">https://ianthehenry.com/posts/my-kind-of-repl/</guid></item><item><title>Generalized Macros</title><description>&lt;p>I&amp;rsquo;ve been &lt;a href="https://ianthehenry.com/posts/janet-for-mortals/">writing&lt;/a> a &lt;a href="https://ianthehenry.com/posts/why-janet/">lot&lt;/a> of &lt;a href="https://janet-lang.org/">Janet&lt;/a> lately, and I&amp;rsquo;ve been especially enjoying my time with &lt;a href="https://ianthehenry.com/posts/janet-game/the-problem-with-macros/">the macro system&lt;/a>.&lt;/p>
&lt;p>Janet macros are Common Lisp-flavored unhygienic &lt;code>gensym&lt;/code>-style macros. They are extremely powerful, and very easy to write, but they can be &lt;a href="https://janet.guide/macro-mischief/">pretty tricky to get right&lt;/a>. It&amp;rsquo;s easy to make mistakes that lead to unwanted variable capture, or to write macros that only work if they&amp;rsquo;re expanded in particular contexts, and it can be pretty difficult to detect these problems ahead of time.&lt;/p>
&lt;p>So people have spent a lot of time thinking about ways to write macros more safely &amp;ndash; sometimes at the cost of expressiveness or simplicity &amp;ndash; and almost all recent languages use some sort of hygienic macro system that defaults to doing the right thing.&lt;/p>
&lt;p>But as far as I know, no one has approached macro systems from the other direction. No one looked at Common Lisp&amp;rsquo;s macros and said &amp;ldquo;What if these macros &lt;em>aren&amp;rsquo;t dangerous enough?&lt;/em> What if we could make them even &lt;em>harder&lt;/em> to write correctly, in order to &lt;em>marginally&lt;/em> increase their power and expressiveness?&amp;rdquo;&lt;/p>
&lt;p>So welcome to my blog post.&lt;/p>
&lt;p>I want to show you an idea for a new kind of macro. A macro that can not only rewrite &lt;em>itself&lt;/em>, but actually rewrite &lt;em>any&lt;/em> form in your entire program.&lt;/p>
&lt;p>I think that &amp;ldquo;what, no, why on earth&amp;rdquo; is an entirely reasonable response to that statement, so let&amp;rsquo;s take a look at a motivating example together.&lt;/p>
&lt;p>Lots of languages have something like &lt;code>defer&lt;/code> that will run an expression at the end of a block, whether or not the rest of the code raises an exception. &lt;code>defer&lt;/code> is just like wrapping the rest of the function in a try-finally block, except that, well, you don&amp;rsquo;t actually have to do any wrapping. Which means no indentation increase, and no extra nested parentheses.&lt;/p>
&lt;p>Here&amp;rsquo;s an example of what &lt;code>defer&lt;/code> might look in Janet:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(do
(def f (file/open &amp;quot;foo.txt&amp;quot;))
(defer (file/close f))
(def contents (file/read f))
(do-something-dangerous-with contents))
&lt;/code>&lt;/pre>&lt;p>Now, we can&amp;rsquo;t implement &lt;code>defer&lt;/code> as a traditional macro, because a traditional macro can only rewrite &lt;em>itself&lt;/em>. But what we really want to do is rewrite the parent form that the &lt;code>defer&lt;/code> appears in, to wrap its &lt;em>siblings&lt;/em> in a finally expression:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(do
(def f (file/open &amp;quot;foo.txt&amp;quot;))
(finally
(do
(def contents (file/read f))
(do-something-dangerous-with contents))
(file/close f)))
&lt;/code>&lt;/pre>&lt;p>So generalized macros let us write exactly this.&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;/p>
&lt;p>I want to start at the punchline and work backwards, because my favorite part is how &lt;em>simple&lt;/em> it is to write this. So although I don&amp;rsquo;t actually expect this to make any sense yet, let&amp;rsquo;s go ahead and look at the implementation of this &lt;code>defer&lt;/code> macro:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defmacro defer [] [expr]
(macro lefts rights
~(,;lefts (finally (do ,;rights) ,expr))))
&lt;/code>&lt;/pre>&lt;p>Pretty tame, right?&lt;/p>
&lt;p>So in order to do understand how this works, we&amp;rsquo;ll have to change three things about macros:&lt;/p>
&lt;ol>
&lt;li>Macros no longer have to appear at the head of the form; they can appear anywhere within a form.&lt;/li>
&lt;li>Macros now have two argument lists: the forms &amp;ldquo;to the left&amp;rdquo; of the macro, and the forms &amp;ldquo;to the right&amp;rdquo; of the macro.&lt;/li>
&lt;li>Macros can either return new abstract syntax trees &amp;ndash; like a traditional macro &amp;ndash; or they can return new, anonymous &lt;em>macros&lt;/em> as first-class values.&lt;/li>
&lt;/ol>
&lt;p>Let&amp;rsquo;s go through these ideas one at a time in slightly more detail, and then we&amp;rsquo;ll circle back to the definition of &lt;code>defer&lt;/code>.&lt;/p>
&lt;h1 id="macros-can-appear-anywhere">Macros can appear anywhere&lt;/h1>
&lt;p>Scheme already has something called &amp;ldquo;identifier macros,&amp;rdquo; which can appear anywhere within a form. You can use them to say that &lt;code>foo&lt;/code> is a macro, and it can appear in any expression context, and then make &lt;code>(+ 1 foo)&lt;/code> expand to something like &lt;code>(+ 1 (some complicated expression))&lt;/code>.&lt;/p>
&lt;p>But identifier macros can still only rewrite &lt;em>themselves&lt;/em>. In order to do anything interesting with this, we need to add&amp;hellip;&lt;/p>
&lt;h1 id="macros-can-see-forms-to-the-left-and-to-the-right">Macros can see forms &amp;ldquo;to the left&amp;rdquo; and &amp;ldquo;to the right&amp;rdquo;&lt;/h1>
&lt;p>On the face of it this might sound like I&amp;rsquo;m trying to introduce &amp;ldquo;infix macros,&amp;rdquo; so that you could write something like &lt;code>(1 + 2)&lt;/code> and rewrite that to the traditional &lt;code>(+ 1 2)&lt;/code> syntax. And, to be clear, that &lt;em>is&lt;/em> a thing you can do:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defmacro + [left] [right]
[+ left right])
&lt;/code>&lt;/pre>&lt;p>I think that infix macros could be very useful &amp;ndash; we&amp;rsquo;ll talk more about that in a bit &amp;ndash; even if infix &lt;em>math&lt;/em> is not particularly compelling to someone practiced in the prefixual arts.&lt;/p>
&lt;p>But the real reason to support &amp;ldquo;infix&amp;rdquo; macros is for cases like &lt;code>defer&lt;/code>, where the &lt;code>(defer ...)&lt;/code> expression occurs in the middle of a form, and acts as sort of an &amp;ldquo;infix&amp;rdquo; expansion point. But in order for that to work, we need to add&amp;hellip;&lt;/p>
&lt;h1 id="first-class-macros">First-class macros&lt;/h1>
&lt;p>I think this is the trickiest part to wrap your head around, but it&amp;rsquo;s the most important. This is the trick that allows macros to rewrite not only themselves, but also the forms around themselves &amp;ndash; their parents, their grandparents, their&amp;hellip; cousins? I guess? Any other form in your program, actually.&lt;/p>
&lt;p>So the idea is that we can create first-class anonymous macros, and return them from our macro implementations. And then those macros will get expanded &lt;em>in the context of the parent form&lt;/em> that they now appear in.&lt;/p>
&lt;p>This is a lot like returning an anonymous function, except that functions are perfectly reasonable values to put in your abstract syntax trees&amp;hellip; so it&amp;rsquo;s like returning a &lt;em>special&lt;/em> function, a function with a little tag attached that says &amp;ldquo;hey, I&amp;rsquo;m not a real runtime value, I&amp;rsquo;m a macro, so you should call me before you finish macro expansion.&amp;rdquo;&lt;/p>
&lt;p>And just to be super explicit: this is different from a macro returning a &lt;em>syntax tree&lt;/em> that contains another macro invocation. You can already write &amp;ldquo;recursive macros,&amp;rdquo; or macros that return &lt;em>invocations of&lt;/em> other macros. But by creating actual new first-class macros at expansion time, you can close over macro arguments and reference them during the next phase of expansion.&lt;/p>
&lt;p>So with these changes in mind, let&amp;rsquo;s come back to the implementation of &lt;code>defer&lt;/code>:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defmacro defer [] [expr]
(macro lefts rights
~(,;lefts (finally (do ,;rights) ,expr))))
&lt;/code>&lt;/pre>&lt;p>Our &lt;code>defer&lt;/code> macro takes two binding forms: &lt;code>[]&lt;/code> and &lt;code>[expr]&lt;/code>. So it expects no arguments to the left &amp;ndash; the word &lt;code>defer&lt;/code> has to appear at the beginning of its form &amp;ndash; and it expects exactly one argument to its right. In other words, it looks like a normal, traditional prefix macro of one argument.&lt;/p>
&lt;p>But then it returns an anonymous macro that closes over its &lt;code>expr&lt;/code> argument. So if we just look at one step of the expansion, we&amp;rsquo;ll see an abstract syntax tree that looks like this:&lt;/p>
&lt;pre tabindex="0">&lt;code>(do
(def f (file/open &amp;quot;foo.txt&amp;quot;))
&amp;lt;macro&amp;gt;
(def contents (file/read f))
(do-something-dangerous-with contents))
&lt;/code>&lt;/pre>&lt;p>But macro expansion isn&amp;rsquo;t over. After expanding the &lt;code>(defer ...)&lt;/code> form, the macro expander will notice that it expanded to another macro, so it will expand &lt;em>that&lt;/em>. Which winds up invoking our anonymous macro, passing it &lt;code>['do '(def f ...)]&lt;/code> as its &amp;ldquo;left&amp;rdquo; arguments and &lt;code>['(def contents ...) '(do-something...)]&lt;/code> as its &amp;ldquo;right&amp;rdquo; arguments.&lt;/p>
&lt;p>And then that will return a replacement for the entire &lt;code>(do ...)&lt;/code> form, giving us our final result:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(do
(def f (file/open &amp;quot;foo.txt&amp;quot;))
(finally
(do
(def contents (file/read f))
(do-something-dangerous-with contents))
(file/close f)))
&lt;/code>&lt;/pre>&lt;p>Neat, right?&lt;/p>
&lt;p>This type of macro gives us a lot more freedom to decide how we want our code to look. I&amp;rsquo;m honestly not really sure &lt;em>how much&lt;/em> more freedom, because I haven&amp;rsquo;t spent very much time with the idea yet. But I&amp;rsquo;ve been thinking about it for a while, and I&amp;rsquo;ve come up with a few examples of things that we can do with this &amp;ndash; some much dumber than others.&lt;/p>
&lt;p>Let&amp;rsquo;s take a look at a few of them.&lt;/p>
&lt;h1 id="nest-less">Nest less&lt;/h1>
&lt;p>I think that reducing the number of nested parentheses and general indentation might be the most compelling use case for this sort of macro. I mean, really this is all &lt;code>defer&lt;/code> does: it lets you write &lt;code>finally&lt;/code> with a little less nesting, and with the expressions in a slightly different order.&lt;/p>
&lt;p>I spend most of my programming time writing OCaml. OCaml doesn&amp;rsquo;t have &amp;ldquo;block scope&amp;rdquo; or &amp;ldquo;function scope&amp;rdquo; like most languages &amp;ndash; it has &lt;em>expression scope&lt;/em>. You introduce new &amp;ldquo;variables&amp;rdquo; (they can&amp;rsquo;t actually &lt;em>vary&lt;/em>; all OCaml bindings are &amp;ldquo;&lt;code>const&lt;/code>&amp;quot;) using &lt;code>let ... in&lt;/code>, and the binding only exists on the right-hand side of that particular expression.&lt;/p>
&lt;p>If you think about the nesting of the OCaml abstract syntax tree, it looks something like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ocaml" data-lang="ocaml">&lt;span class="o">(&lt;/span>&lt;span class="k">let&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">10&lt;/span> &lt;span class="k">in&lt;/span>
&lt;span class="o">(&lt;/span>&lt;span class="k">let&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">20&lt;/span> &lt;span class="k">in&lt;/span>
&lt;span class="o">(&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="o">)))&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>But of course you don&amp;rsquo;t write OCaml like that. For one thing, the parentheses are redundant:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ocaml" data-lang="ocaml">&lt;span class="k">let&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">10&lt;/span> &lt;span class="k">in&lt;/span>
&lt;span class="k">let&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">20&lt;/span> &lt;span class="k">in&lt;/span>
&lt;span class="n">x&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">y&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>For another thing, this triangular indentation is really annoying. So you actually write it like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ocaml" data-lang="ocaml">&lt;span class="k">let&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">10&lt;/span> &lt;span class="k">in&lt;/span>
&lt;span class="k">let&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">20&lt;/span> &lt;span class="k">in&lt;/span>
&lt;span class="n">x&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">y&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>The parse tree for that expression is still nested, but it doesn&amp;rsquo;t &lt;em>look&lt;/em> nested &amp;ndash; you always format your code linearly.&lt;/p>
&lt;p>So lisps also have &lt;code>let&lt;/code>, but lisps don&amp;rsquo;t have the luxury of leaving off the parentheses, so we&amp;rsquo;re back to the first example:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(let [x 10]
(let [y 20]
(+ x y)))
&lt;/code>&lt;/pre>&lt;p>And if we tried to write that without indentation, then&amp;hellip;&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(let [x 10]
(let [y 20]
(+ x y)))
&lt;/code>&lt;/pre>&lt;p>Immediate aneurysm.&lt;/p>
&lt;p>Fortunately, every lisp dialect that I know of mitigates this problem substantially by allowing &lt;code>let&lt;/code> to introduce multiple bindings in a single form:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(let [x 10
y 20]
(+ x y))
&lt;/code>&lt;/pre>&lt;p>Which means that we only have to increase the nesting by one level in the very common case that we have a series of &lt;code>let&lt;/code> expressions. But by using generalized macros instead, we can write a version of &lt;code>let&lt;/code> that doesn&amp;rsquo;t increase indentation at all. I&amp;rsquo;ll call it &lt;code>def&lt;/code>:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defmacro def [] [name value]
(macro lefts rights
~(,;lefts (let [,name ,value] ,;rights))))
&lt;/code>&lt;/pre>&lt;p>&lt;code>def&lt;/code> lets us write code like this:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(def x 10)
(def y 20)
(+ x y)
&lt;/code>&lt;/pre>&lt;p>Which gets transformed into code like this:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(let [x 10]
(let [y 20]
(+ x y)))
&lt;/code>&lt;/pre>&lt;p>Of course Janet &amp;ndash; and most lisps &amp;ndash; have a &amp;ldquo;linear assignment&amp;rdquo; form like this built into the language. In Janet it&amp;rsquo;s called &amp;ndash; &lt;em>coincidentally enough&lt;/em> &amp;ndash; &lt;code>def&lt;/code>.&lt;/p>
&lt;p>In fact in Janet, &lt;code>def&lt;/code> is actually the &lt;em>primitive&lt;/em> way to create new bindings, and &lt;code>let&lt;/code> is a macro that just desugars to &lt;code>do&lt;/code> + &lt;code>def&lt;/code>, which is very reasonable and pragmatic, but feels &lt;em>weird&lt;/em> to me.&lt;/p>
&lt;p>It feels weird to me because, in my mind, &lt;code>let&lt;/code> should be syntax sugar for &lt;code>fn&lt;/code> &amp;ndash; Janet&amp;rsquo;s word for &lt;code>lambda&lt;/code>. Because, after all, these two expressions are equivalent:&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(let [x 10] (+ x 1))
((fn [x] (+ x 1)) 10)
&lt;/code>&lt;/pre>&lt;p>&lt;code>let&lt;/code> allows us to write the expression in a much more natural order, but we can introduce new bindings without any &lt;code>let&lt;/code>s at all.&lt;/p>
&lt;p>This might sound like weird mathematical lambda calculus trivia, but it&amp;rsquo;s not: it&amp;rsquo;s important to understand introducing new variables as a special-case of function application, even if this &lt;em>particular&lt;/em> function application happens to be trivial.&lt;/p>
&lt;p>Because we can apply the same technique that we just used &amp;ndash; rewriting &lt;code>def&lt;/code> to &lt;code>let&lt;/code>, and rewriting &lt;code>let&lt;/code> to &lt;code>fn&lt;/code> &amp;ndash; to do something much more interesting.&lt;/p>
&lt;h2 id="generalized-function-application">Generalized function application&lt;/h2>
&lt;p>So Haskell has something called &lt;code>do&lt;/code> notation. You&amp;rsquo;ve probably seen something like this before:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-haskell" data-lang="haskell">&lt;span class="nf">addAll&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="kt">Int&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="kt">Int&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="kt">Int&lt;/span>&lt;span class="p">]&lt;/span>
&lt;span class="nf">addAll&lt;/span> &lt;span class="n">xs&lt;/span> &lt;span class="n">ys&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kr">do&lt;/span>
&lt;span class="n">x&lt;/span> &lt;span class="ow">&amp;lt;-&lt;/span> &lt;span class="n">xs&lt;/span>
&lt;span class="n">y&lt;/span> &lt;span class="ow">&amp;lt;-&lt;/span> &lt;span class="n">ys&lt;/span>
&lt;span class="n">return&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;pre tabindex="0">&lt;code>ghci&amp;gt; addAll [1, 2] [10, 20]
[11,21,12,22]
&lt;/code>&lt;/pre>&lt;p>This is equivalent to the following Janet code:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defn add-all [xs ys]
(mapcat (fn [x]
(mapcat (fn [y]
[(+ x y)])
ys))
xs))
&lt;/code>&lt;/pre>&lt;p>But I think the Haskell code is easier to read. Partly that&amp;rsquo;s because the argument order to Janet&amp;rsquo;s &lt;code>mapcat&lt;/code> function makes the values we&amp;rsquo;re traversing appear in reverse order in our source code, and we could fix this by redefining &lt;code>mapcat&lt;/code> with a argument different order:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defn add-all [xs ys]
(mapcat xs (fn [x]
(mapcat ys (fn [y]
[(+ x y)])))))
&lt;/code>&lt;/pre>&lt;p>This reminds me of the transformation we did when we changed &lt;code>((fn [x] (+ x 1)) 10)&lt;/code> into &lt;code>(let [x 10] (+ x 1))&lt;/code>. So what if we take it one step further, and do the same thing we did to get &lt;code>def&lt;/code>?&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defn add-all [xs ys]
(as x mapcat xs)
(as y mapcat ys)
[(+ x y)])
&lt;/code>&lt;/pre>&lt;p>It&amp;rsquo;s not quite as concise as Haskell&amp;rsquo;s &lt;code>do&lt;/code> notation: Haskell is able to use the type of the expression to determine what &lt;code>&amp;lt;-&lt;/code> means, so there&amp;rsquo;s no need to specify the &lt;code>mapcat&lt;/code> bit: it&amp;rsquo;s implied from the fact that we gave it a list.&lt;/p>
&lt;p>Janet doesn&amp;rsquo;t have an analog for &lt;a href="https://en.wikipedia.org/wiki/Type_class">type classes&lt;/a>, so we have to be a little more explicit, but this means that we can do more than just &amp;ldquo;bind&amp;rdquo; with the &lt;code>as&lt;/code> macro. We can also &lt;code>map&lt;/code>:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defn add-all [xs ys]
(as x mapcat xs)
(as y map ys)
(+ x y))
&lt;/code>&lt;/pre>&lt;p>Implementing &lt;code>as&lt;/code> is just as easy as implementing &lt;code>def&lt;/code>:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defmacro as [] [name f arg]
(macro lefts rights
~(,;lefts (,f (fn [,name] ,;rights) ,arg))))
&lt;/code>&lt;/pre>&lt;p>If you haven&amp;rsquo;t programmed in a language like Haskell, this particular syntax sugar might seem a little odd at first. But a specialized notation for generalized function application is extremely useful &amp;ndash; we have it in OCaml too, through a syntax extension called &lt;a href="https://github.com/janestreet/ppx_let">&lt;code>ppx_let&lt;/code>&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ocaml" data-lang="ocaml">&lt;span class="k">let&lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="n">bind&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">xs&lt;/span> &lt;span class="k">in&lt;/span>
&lt;span class="k">let&lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="n">bind&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ys&lt;/span> &lt;span class="k">in&lt;/span>
&lt;span class="n">return&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="o">)&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>I think OCaml&amp;rsquo;s notation is actually more clear than Haskell&amp;rsquo;s &amp;ndash; it highlights the symmetry between &amp;ldquo;ordinary&amp;rdquo; let bindings and &amp;ldquo;fancy&amp;rdquo; let bindings like these. And because it can do more than just &lt;code>bind&lt;/code>, we can also avoid the explicit &lt;code>return&lt;/code> in OCaml:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ocaml" data-lang="ocaml">&lt;span class="k">let&lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="n">bind&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">xs&lt;/span> &lt;span class="k">in&lt;/span>
&lt;span class="k">let&lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="n">map&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ys&lt;/span> &lt;span class="k">in&lt;/span>
&lt;span class="n">x&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">y&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>map&lt;/code> and &lt;code>bind&lt;/code> aren&amp;rsquo;t the only functions of this variety, either. Although monads are ubiquitous in OCaml, I spend a lot of my time &lt;a href="https://opensource.janestreet.com/bonsai/">working with arrows&lt;/a> as well. And arrows have yet another notation: &lt;code>let%sub&lt;/code> and &lt;code>let%arr&lt;/code>.&lt;/p>
&lt;p>All of these are generalizations of regular function application. Without worrying about what any of this means, just look at how similar the &lt;em>shape&lt;/em> of these different type signatures are:&lt;/p>
&lt;pre tabindex="0">&lt;code>val (@@) : 'a -&amp;gt; ('a -&amp;gt; 'b) -&amp;gt; 'b
val map : 'a f -&amp;gt; ('a -&amp;gt; 'b) -&amp;gt; 'b f
val bind : 'a f -&amp;gt; ('a -&amp;gt; 'b f) -&amp;gt; 'b f
val sub : 'a s -&amp;gt; ('a r -&amp;gt; 'b s) -&amp;gt; 'b s
val arr : 'a r -&amp;gt; ('a -&amp;gt; 'b) -&amp;gt; 'b s
&lt;/code>&lt;/pre>&lt;p>Okay, I know; this isn&amp;rsquo;t supposed to be a blog post about monads or arrows. Let&amp;rsquo;s get back to macros.&lt;/p>
&lt;h1 id="infix-operators">Infix operators&lt;/h1>
&lt;p>So Janet has some very useful &amp;ldquo;threading&amp;rdquo; macros that allow you to write code in a more &amp;ldquo;linear&amp;rdquo; fashion than you could without them. They&amp;rsquo;re useful when you&amp;rsquo;re performing a series of transformations to a value:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(filter |(string/has-prefix? &amp;quot;a&amp;quot; $)
(map string/ascii-lower
(map |($ :name)
people)))
&lt;/code>&lt;/pre>&lt;p>With the power of threading macros, you could write that like this instead:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(-&amp;gt;&amp;gt; people
(map |($ :name))
(map string/ascii-lower)
(filter |(string/has-prefix? &amp;quot;a&amp;quot; $)))
&lt;/code>&lt;/pre>&lt;p>This is a lot like &amp;ldquo;method chaining&amp;rdquo; in other languages:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="nx">people&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">map&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">person&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">person&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">map&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">toLowerCase&lt;/span>&lt;span class="p">())&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">startsWith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;a&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>It&amp;rsquo;s easier for me to read the linear notation that uses the threading macro. But it&amp;rsquo;s not actually any easier to &lt;em>write&lt;/em> it.&lt;/p>
&lt;p>I like that method chaining allows me to write the code in the order of the operations: &amp;ldquo;Start with people, get the name, lowercase it, filter it to names that start with &amp;lsquo;a.'&amp;rdquo;&lt;/p>
&lt;p>When writing the threading macro, though, the way you &lt;em>type&lt;/em> this is &amp;ldquo;start with people, okay wait, go back, surround it in parentheses, add a &lt;code>-&amp;gt;&amp;gt;&lt;/code> at the beginning, now move the cursor to the end of the form, and then get the name&amp;hellip;&amp;rdquo;&lt;/p>
&lt;p>I don&amp;rsquo;t like that. And I know that there are &lt;em>fancy&lt;/em> editors that allow you to easily wrap expressions in threading macros or any other without repositioning the cursor, but I&amp;rsquo;d rather use a syntax that doesn&amp;rsquo;t require a structural editor to work with comfortably.&lt;/p>
&lt;p>So here&amp;rsquo;s another way to write this:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(people
@ (map |($ :name))
@ (map string/ascii-lower)
@ (filter |(string/has-prefix? &amp;quot;a&amp;quot; name)))
&lt;/code>&lt;/pre>&lt;p>This uses &lt;code>@&lt;/code> as an infix function application macro. I prefer &lt;code>|&lt;/code> myself, and that&amp;rsquo;s the notation that I chose for &lt;a href="https://bauble.studio/">Bauble&lt;/a>, but &lt;code>|&lt;/code> is how you create short anonymous functions in Janet, so I don&amp;rsquo;t want to step on that.&lt;/p>
&lt;p>The main reason I prefer this notation is that it&amp;rsquo;s easier for me to &lt;em>type&lt;/em>. I don&amp;rsquo;t know that it&amp;rsquo;s any easier to &lt;em>read&lt;/em> than &lt;code>-&amp;gt;&lt;/code>, but it allows me to write code in the order that I think it, and I like being able to choose a syntax that maps neatly onto my brain.&lt;/p>
&lt;p>Another infix macro that I like is &lt;code>.&lt;/code>. &lt;code>.&lt;/code> is a convenient macro for looking up a keyword in a struct or table, so that you can write &lt;code>struct.key&lt;/code> instead of &lt;code>(get struct :key)&lt;/code>.&lt;/p>
&lt;p>In Janet &lt;code>struct.key&lt;/code> parses as a single symbol, so we can&amp;rsquo;t actually implement this as a generalized macro without a separate preprocessing step to split it into three symbols. But we &lt;em>can&lt;/em> use it as &lt;code>struct . key&lt;/code>, which parses as three separate symbols:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defmacro . lefts [key &amp;amp; rights]
~(,;(drop-last lefts)
(get ,(last lefts) ,(keyword key))
,;rights))
&lt;/code>&lt;/pre>&lt;p>Which we can then use like this:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(print foo . bar)
&lt;/code>&lt;/pre>&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(print (get foo :bar))
&lt;/code>&lt;/pre>&lt;p>This is very goofy looking, but you could imagine a language where &lt;code>.&lt;/code> always parsed as its own symbol, so that we could just write &lt;code>foo.bar&lt;/code> and have that expand to &lt;code>(get foo :bar)&lt;/code> automatically.&lt;/p>
&lt;p>Another infix macro that I think could be interesting is &lt;code>:&lt;/code>, to create a pair.&lt;/p>
&lt;p>Janet already uses &lt;code>:&lt;/code> as a leader character for declaring &lt;em>keywords&lt;/em>, so this won&amp;rsquo;t work in Janet. But you could imagine, again, a different language where &lt;code>:&lt;/code> is used as a short way to create a pair of two elements. So:&lt;/p>
&lt;pre>&lt;code>foo:bar
&lt;/code>&lt;/pre>
&lt;p>Would become:&lt;/p>
&lt;pre>&lt;code>(foo bar)
&lt;/code>&lt;/pre>
&lt;p>This might be useful in languages that use wrapped &lt;code>let&lt;/code>s, where you have to write:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(let ((x 10)
(y 20))
(+ x y))
&lt;/code>&lt;/pre>&lt;p>Instead, you could write that as:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(let (x:10 y:20)
(+ x y))
&lt;/code>&lt;/pre>&lt;p>But have it parse in exactly the same way.&lt;/p>
&lt;p>You could imagine it in &lt;code>cond&lt;/code> as well:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(cond
(&amp;gt; x 0): &amp;quot;positive&amp;quot;
(&amp;lt; x 0): &amp;quot;negative&amp;quot;
(= x 0): &amp;quot;zero&amp;quot;
true: &amp;quot;nan&amp;quot;)
&lt;/code>&lt;/pre>&lt;p>Of course Janet doesn&amp;rsquo;t require wrapping each entry in a &lt;code>cond&lt;/code> expression in parentheses, so this isn&amp;rsquo;t as compelling in Janet.&lt;/p>
&lt;p>For a slight variation on this, imagine a macro called &lt;code>::&lt;/code>. It&amp;rsquo;s just like &lt;code>:&lt;/code> &amp;ndash; it creates a pair &amp;ndash; but the pair appears in reverse order from how you write it. We&amp;rsquo;re well off the Janet path now, but we could use this as a concise notation for adding type annotations without an explosion of parentheses.&lt;/p>
&lt;p>Let&amp;rsquo;s say we have a &amp;ndash; function? macro? &amp;ndash; &lt;em>something&lt;/em> called &lt;code>Int&lt;/code> that provides a type annotation to our compiler. We&amp;rsquo;d normally write the type of an expression like this:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(def x (Int (compute-thing)))
&lt;/code>&lt;/pre>&lt;p>But look all those close parens! I don&amp;rsquo;t want to balance those. So instead, we could write:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(def x (compute-thing) :: Int)
&lt;/code>&lt;/pre>&lt;p>Which is equivalent to the less intuitive (to me):&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(def x Int:(compute-thing))
&lt;/code>&lt;/pre>&lt;p>&lt;code>::&lt;/code> doesn&amp;rsquo;t mean &amp;ldquo;type annotation,&amp;rdquo; though, it just means &amp;ldquo;wrap in parentheses.&amp;rdquo; We could use it to do dumber things:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">&amp;quot;hello&amp;quot; :: print
&lt;/code>&lt;/pre>&lt;p>Which would expand, of course, to &lt;code>(print &amp;quot;hello&amp;quot;)&lt;/code>. But&amp;hellip; I don&amp;rsquo;t know why you would want to do that.&lt;/p>
&lt;h1 id="comment">Comment&lt;/h1>
&lt;p>Something that I occasionally wish for is a &lt;code>(comment ...)&lt;/code> macro that lets me ignore code.&lt;/p>
&lt;p>You can&amp;rsquo;t actually write such a macro in Janet. Janet &lt;em>has&lt;/em> a macro called &lt;code>comment&lt;/code> in the standard library, but &lt;code>comment&lt;/code> always expands to &lt;em>nil&lt;/em>, and &lt;code>nil&lt;/code> is not &lt;em>nothing&lt;/em>. This means there are lots of places you can&amp;rsquo;t use &lt;code>(comment ...)&lt;/code>:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defn foo [a (comment this is a comment)]
(print a))
&lt;/code>&lt;/pre>&lt;p>If you tried to compile that, you&amp;rsquo;d get an error:&lt;/p>
&lt;pre tabindex="0">&lt;code>compile error: unexpected type in destruction, got nil
&lt;/code>&lt;/pre>&lt;p>Because after macro expansion, the compiler actually sees:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defn foo [a nil]
(print a))
&lt;/code>&lt;/pre>&lt;p>Which is not valid.&lt;/p>
&lt;p>With generalized macros, though, you can write a comment that actually disappears:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defmacro comment [] [&amp;amp;]
(macro lefts rights [;lefts ;rights]))
(defn foo [a (comment this is a comment)]
(print a))
&lt;/code>&lt;/pre>&lt;p>After expansion, that will just be:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defn foo [a]
(print a))
&lt;/code>&lt;/pre>&lt;p>Like it never happened.&lt;/p>
&lt;h1 id="ifelse">&lt;code>if&lt;/code>/&lt;code>else&lt;/code>&lt;/h1>
&lt;p>One thing that sometimes trips me up when I&amp;rsquo;m writing Janet is &lt;code>if&lt;/code>.&lt;/p>
&lt;p>&lt;code>if&lt;/code> takes three forms: a boolean expression, an expression to evaluate if it&amp;rsquo;s truthy, and an expression to evaluate if it&amp;rsquo;s falsy. Which is nice and concise, but it&amp;rsquo;s different enough from other languages that I use &amp;ndash; languages with explicit &lt;code>else&lt;/code>s &amp;ndash; that sometimes I&amp;rsquo;ll write code like this by mistake:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(if should-do-something-important
(print &amp;quot;okay, performing important work:&amp;quot;)
(perform-important-work))
&lt;/code>&lt;/pre>&lt;p>That actually &lt;em>doesn&amp;rsquo;t&lt;/em> perform important work &amp;ndash; &lt;code>(perform-important-work)&lt;/code> is the &amp;ldquo;else&amp;rdquo; section of that conditional. In order to do more than one thing in the &amp;ldquo;then&amp;rdquo; branch, we have to wrap all of the statements in &lt;code>do&lt;/code>.&lt;/p>
&lt;p>And of course Janet has a &lt;code>when&lt;/code> macro that does exactly what I want:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(when should-do-something-important
(print &amp;quot;okay, performing important work:&amp;quot;)
(perform-important-work))
&lt;/code>&lt;/pre>&lt;p>Which doesn&amp;rsquo;t have an &lt;code>else&lt;/code> branch, and usually when I&amp;rsquo;m writing an &lt;code>if&lt;/code> without an &lt;code>else&lt;/code> I should just use &lt;code>when&lt;/code> in the first place.&lt;/p>
&lt;p>But.&lt;/p>
&lt;p>Generalized macros actually let us write &lt;code>if&lt;/code> with an explicit &lt;code>else&lt;/code>. I&amp;rsquo;m not saying this is a &lt;em>good idea&lt;/em>, but they let us write something like:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(if (empty? name)
(print &amp;quot;you must provide a valid name&amp;quot;))
(else
(print &amp;quot;okay i think it checks out&amp;quot;))
&lt;/code>&lt;/pre>&lt;p>This example is a little weird because, in order for this to work nicely, we&amp;rsquo;ll have to rename the built-in &lt;code>if&lt;/code>. I&amp;rsquo;ll call the ternary version &lt;code>if-then-else&lt;/code> for this example, and say that &lt;code>if&lt;/code> now means the same thing as Janet&amp;rsquo;s &lt;code>when&lt;/code>.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defmacro else [] [else-exprs]
(macro lefts rights
(match (last lefts)
['if &amp;amp; then-exprs]
~(,;(drop-last lefts)
(if-then-else (do ,;then-exprs) (do ,;else-exprs))
,;rights)
(error &amp;quot;else must come immediately after if&amp;quot;))))
&lt;/code>&lt;/pre>&lt;p>This is interesting because the &lt;code>else&lt;/code> macro actually rewrites the form &lt;em>before&lt;/em> itself, changing the &lt;code>if&lt;/code> to an &lt;code>if-then-else&lt;/code> and then fussing with its arguments.&lt;/p>
&lt;h1 id="weirder-more-exotic-things">Weirder, more exotic things&lt;/h1>
&lt;p>I could keep going, but, as you have probably noticed, this is already a very long blog post. There&amp;rsquo;s a lot more that you can do with generalized macros &amp;ndash; some of it useful, some of it unhinged, and we don&amp;rsquo;t have time to talk about all of it.&lt;/p>
&lt;p>So far we&amp;rsquo;ve only seen macros that rewrite their parents or immediate siblings, but you can write macros that return macros that return macros, and use them to rewrite arbitrary forms anywhere in your program. You could write a macro that rewrites &amp;ldquo;the nearest enclosing function definition,&amp;rdquo; recursively accumulating first-class macros until finally expanding all of them.&lt;/p>
&lt;p>You can write actual left- and right-associative infix operators, and I think that if you tried hard enough, you could even use dynamic variables and controlled macro expansion to implement infix operator precedence (although I don&amp;rsquo;t think you &lt;em>should&lt;/em>).&lt;/p>
&lt;p>You could implement (a weaker version of) Janet&amp;rsquo;s &lt;code>splice&lt;/code> built-in as a generalized macro. You could implement &amp;ldquo;identifier macros&amp;rdquo; that look around themselves and expand to something different when they appear as the first argument to a &lt;code>(set ...)&lt;/code> form. You could, you could&amp;hellip;&lt;/p>
&lt;p>You could do a lot of things, but I&amp;rsquo;m going to have to leave these as exercises to the reader, because it&amp;rsquo;s time to switch gears and talk about why you &lt;em>shouldn&amp;rsquo;t&lt;/em> do this.&lt;/p>
&lt;h1 id="problem-one-the-repl">Problem one: the repl&lt;/h1>
&lt;p>The main reason that this seems like a bad idea is that macros like this don&amp;rsquo;t work at the top level.&lt;/p>
&lt;p>If you&amp;rsquo;re just using the repl, and you type &lt;code>(defer (file/close f))&lt;/code>, what happens? One of the arguments to that macro is &amp;ldquo;everything to the right.&amp;rdquo; But there isn&amp;rsquo;t anything to the right! At least, not yet. And it won&amp;rsquo;t be able to supply &lt;em>everything&lt;/em> to the right until you stop typing altogether.&lt;/p>
&lt;p>This might not seem like a big deal for &lt;code>defer&lt;/code> &amp;ndash; just don&amp;rsquo;t use &lt;code>defer&lt;/code> at the repl &amp;ndash; but it is a big deal for, say, &lt;code>def&lt;/code>. And I don&amp;rsquo;t know an elegant way to solve this problem: in the general case, macros could look arbitrarily far ahead, so we&amp;rsquo;d have to wait until we closed the repl session to be able to expand them. And that&amp;rsquo;s kinda gross.&lt;/p>
&lt;h1 id="problem-two-generalized-macros-dont-always-compose">Problem two: generalized macros don&amp;rsquo;t always compose&lt;/h1>
&lt;p>All of the examples that we&amp;rsquo;ve seen so far play nicely together, but it&amp;rsquo;s possible to write generalized macros that don&amp;rsquo;t compose with one another.&lt;/p>
&lt;p>The problem is that macro behavior can depend on expansion order. Regular macros always get a chance to run &lt;em>before&lt;/em> their arguments get expanded, which is very convenient. Generalized macros don&amp;rsquo;t have that luxury &amp;ndash; because macros can see the forms around them, the order that you expand those forms matters.&lt;/p>
&lt;p>In my implementation I chose to expand macros in a depth-first, left-to-right order. So macros always see the arguments &amp;ldquo;to the left&amp;rdquo; of themselves fully expanded, and the arguments &amp;ldquo;to the right&amp;rdquo; completely unexpanded.&lt;/p>
&lt;p>And this can be problematic. For example, let&amp;rsquo;s say we make an infix alias for &lt;code>set&lt;/code>, called &lt;code>:=&lt;/code>:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(var x 0)
(x := 1)
&lt;/code>&lt;/pre>&lt;p>Which expands to:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(var x 0)
(set x 1)
&lt;/code>&lt;/pre>&lt;p>This is a trivial generalized macro to write.&lt;/p>
&lt;p>Now let&amp;rsquo;s say we have &lt;em>another&lt;/em> macro, which looks at the form to its left to see if it immediately follows &lt;code>set&lt;/code>. When it does, it rewrites that &lt;code>set&lt;/code> to something else. We could use this to implement some kind of custom associative data structure:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(defmacro at [] [dict key]
(macro lefts rights
(if (= lefts ['set])
~(assign ,dict ,key ,;rights)
~(,;lefts (lookup ,dict ,key) ,;rights))))
&lt;/code>&lt;/pre>&lt;p>So that macro lets us write:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(set (at dict key) 10)
&lt;/code>&lt;/pre>&lt;p>And have that expand to:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(assign dict key 10)
&lt;/code>&lt;/pre>&lt;p>Meanwhile, if we write &lt;code>(print (at dict key))&lt;/code>, that will expand to &lt;code>(print (lookup dict key))&lt;/code>.&lt;/p>
&lt;p>Each of these generalized macros make sense on their own. But if we try to use them together, they just don&amp;rsquo;t work:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">((at dict key) := 1)
&lt;/code>&lt;/pre>&lt;p>Because &lt;code>(at dict key)&lt;/code> expands first. It looks at the forms to its left, sees that there aren&amp;rsquo;t any, so after one step of expansion we have:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">((lookup dict key) := 1)
&lt;/code>&lt;/pre>&lt;p>Then we expand &lt;code>:=&lt;/code>, and finish with:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">(set (lookup dict key) 1)
&lt;/code>&lt;/pre>&lt;p>Which of course was not what we wanted.&lt;/p>
&lt;p>I think that using generalized macros safely requires really understanding the effect that they have on the syntax tree of your program. They&amp;rsquo;re more like &lt;code>-&amp;gt;&amp;gt;&lt;/code> and friends &amp;ndash; explicit syntax re-arrangers &amp;ndash; than they are like other kinds of macros.&lt;/p>
&lt;h1 id="problem-three-you-have-created-in-your-code-a-work-of-madness-that-no-other-human-being-can-possibly-hope-to-understand">Problem three: you have created in your code a work of madness that no other human being can possibly hope to understand&lt;/h1>
&lt;p>ugh not again&lt;/p>
&lt;h1 id="prior-art">Prior art&lt;/h1>
&lt;p>I feel like this approach is so simple that it must have been done before, but I can&amp;rsquo;t find any references to it. That said, I have no idea how to search for it effectively! So if you&amp;rsquo;ve seen this technique before, or if you&amp;rsquo;ve heard of it being used in the past, I&amp;rsquo;d love to hear about it.&lt;/p>
&lt;h1 id="proof-of-concept">Proof of concept&lt;/h1>
&lt;p>I implemented this macro system in Janet, in order to play around with it and test out my macro implementations.&lt;/p>
&lt;p>It was pretty easy to write! It really is a very modest generalization of a traditional macro system. I didn&amp;rsquo;t actually write a custom module loader that would let you &lt;em>use&lt;/em> this as your default macro system in Janet code, but I wrote (the equivalent of) &lt;code>macex&lt;/code>, and adding the custom module loader would be pretty easy if you wanted to use it &amp;ldquo;for real.&amp;rdquo;&lt;/p>
&lt;p>You can look at the code here: &lt;a href="https://github.com/ianthehenry/macaroni">https://github.com/ianthehenry/macaroni&lt;/a>&lt;/p>
&lt;p>Or &lt;a href="https://github.com/ianthehenry/macaroni/tree/master/test">take a peek at some of the tests&lt;/a>, to see the examples in this post in action, as well as some weirder things that &lt;a href="https://github.com/ianthehenry/macaroni/blob/master/test/grandparent.janet">didn&amp;rsquo;t make the cut&lt;/a>.&lt;/p>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>Of course we could teach &lt;code>do&lt;/code> to scan through all of its arguments and look for &lt;code>defer&lt;/code>s, and implement this that way, but then this would only work in &lt;code>do&lt;/code> expressions. What if we want to do this inside a &lt;code>(fn [] ...)&lt;/code> block? Or an &lt;code>if&lt;/code>? Or a &lt;code>while&lt;/code>? By making &lt;code>defer&lt;/code> itself do the transformation, we can use it anywhere &amp;ndash; and make it easy to add new macros that behave like &lt;code>defer&lt;/code>, without having to teach &lt;code>do&lt;/code> about them.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2" role="doc-endnote">
&lt;p>Back in the Old Days, JavaScript only had function-scoped variables, so the &lt;em>only&lt;/em> way to create new bindings in JavaScript code was to create and invoke an anonymous function like this. The pattern was so common that we called them &amp;ldquo;immediately invoked function expressions,&amp;rdquo; and it was a real thing that you had to do in order to, for example, close over distinct values in a loop.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><pubDate>Tue, 18 Apr 2023 00:00:00 +0000</pubDate><link>https://ianthehenry.com/posts/generalized-macros/</link><guid isPermaLink="true">https://ianthehenry.com/posts/generalized-macros/</guid></item><item><title>Why Janet?</title><description>&lt;p>I never thought it could happen to me. I mean, parentheses? In this day and age? But for the past couple years, my go-to programming language for fun side projects has been a little Lisp dialect called &lt;a href="https://janet-lang.org/">Janet&lt;/a>.&lt;/p>
&lt;pre tabindex="0">&lt;code>(print &amp;quot;hey janet&amp;quot;)
&lt;/code>&lt;/pre>&lt;p>I like Janet so much that &lt;a href="https://janet.guide/">I wrote an entire book about it&lt;/a>, and put it on The Internet for free, in the hopes of attracting more Janetors to the language.&lt;/p>
&lt;p>I think you should read it, but I know that you don&amp;rsquo;t believe me, so I&amp;rsquo;m going to try to convince you. Here&amp;rsquo;s my attempt at a sales pitch: here is why you &amp;ndash; &lt;em>you of all people&lt;/em> &amp;ndash; should give Janet a chance.&lt;/p>
&lt;h1 id="janet-is-simple">Janet is simple&lt;/h1>
&lt;p>Janet is an imperative language with first-class functions, a single namespace for identifiers, and lexical block scoping. The core of the language is very small, consisting of only eight instructions: &lt;code>do&lt;/code>, &lt;code>def&lt;/code>, &lt;code>var&lt;/code>, &lt;code>set&lt;/code>, &lt;code>if&lt;/code>, &lt;code>while&lt;/code>, &lt;code>break&lt;/code>, &lt;code>fn&lt;/code>. But thanks to macros, there are lots of high-level wrappers that give you more powerful or convenient control flow.&lt;/p>
&lt;aside>
&lt;p>There are actually five more instructions that exist to support macros: &lt;code>quote&lt;/code>, &lt;code>unquote&lt;/code>, &lt;code>quasiquote&lt;/code>, &lt;code>splice&lt;/code>, and &lt;code>upscope&lt;/code>. But you don&amp;rsquo;t have to write those in &amp;ldquo;regular&amp;rdquo; code.&lt;/p>
&lt;/aside>
&lt;p>You can &amp;ldquo;learn&amp;rdquo; Janet in an afternoon, because the runtime semantics are extremely familiar: think JavaScript, plus value types, minus all the wats. And the rest of the language is small: the entire standard library &lt;a href="https://janet-lang.org/api/index.html">fits on one page&lt;/a>. It was this ease of getting started that got me hooked in the first place.&lt;/p>
&lt;h1 id="janet-is-distributable">Janet is distributable&lt;/h1>
&lt;p>It&amp;rsquo;s easy to compile Janet programs into native executables that statically link the Janet runtime. And you can share those programs with other people, without asking them to install Janet first &amp;ndash; or your project&amp;rsquo;s dependencies, or anything else for that matter. You don&amp;rsquo;t even have to tell them it&amp;rsquo;s written in Janet!&lt;/p>
&lt;p>The way that Janet pulls this off is very elegant: Janet compiles itself to bytecode, and then writes that bytecode into a &lt;code>.c&lt;/code> file that also starts up the Janet runtime. Then it compiles that C file with your system&amp;rsquo;s C compiler. Since Janet is designed to be easy to embed, this makes perfect sense: it is, essentially, embedding itself into a trivial C executable.&lt;/p>
&lt;p>A simple Janet &amp;ldquo;hello world&amp;rdquo; compiled to a native binary weighs under a megabyte (784K for Janet 1.27.0 on aarch64 macOS, but your mileage may vary). This includes the full Janet runtime, garbage collector, and even the bytecode compiler &amp;ndash; so you can write programs that evaluate Janet code at runtime, &lt;a href="https://bauble.studio/">if you want to&lt;/a>.&lt;/p>
&lt;p>This makes Janet an excellent choice for writing little command-line apps. Which is especially true when you consider that&amp;hellip;&lt;/p>
&lt;h1 id="janet-is-unrealistically-good-at-parsing-text">Janet is unrealistically good at parsing text&lt;/h1>
&lt;p>Instead of regular expressions, Janet&amp;rsquo;s text wrangling is based around &lt;em>parsing expression grammars&lt;/em>. Parsing expression grammars are simpler, more powerful, and more predictable than regular expressions. They aren&amp;rsquo;t line-oriented, so they can parse multi-line text without a problem. They can also parse HTML, or JSON, or any other non-regular language. They can also parse &lt;em>binary&lt;/em> file formats &amp;ndash; they have no problems with arbitrary null bytes.&lt;/p>
&lt;p>They really are &lt;em>parsers&lt;/em>: structured, composable, first-class parsers. &lt;a href="https://janet.guide/pegular-expressions/">And they&amp;rsquo;re pretty easy to learn!&lt;/a>&lt;/p>
&lt;h1 id="janet-has-the-best-subprocess-dsl-of-any-high-level-language">Janet has the best subprocess DSL of any high-level language&lt;/h1>
&lt;p>There is a &lt;a href="https://github.com/andrewchambers/janet-sh">third-party library called &lt;code>sh&lt;/code>&lt;/a> that provides a shell scripting DSL that allows you to express pipes and redirects directly in Janet. Like this:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-janet" data-lang="janet">($ find . -name *.janet | say)
&lt;/code>&lt;/pre>&lt;p>It&amp;rsquo;s pretty incredible. It&amp;rsquo;s such a nice DSL that &lt;a href="https://janet.guide/scripting/">I dedicated a whole chapter of &lt;em>Janet for Mortals&lt;/em> to it&lt;/a> &amp;ndash; and the things that you can do with it. It elevates Janet from a reasonable alternative to Perl to a reasonable alternative to &lt;em>Bash&lt;/em> for a surprisingly large range of programs.&lt;/p>
&lt;h1 id="janet-is-embeddable">Janet is embeddable&lt;/h1>
&lt;p>Lua has become the de facto &amp;ldquo;embedded language,&amp;rdquo; which is a shame, because&amp;hellip; well, this isn&amp;rsquo;t a post about Lua. You might not care about this very much, but there&amp;rsquo;s a chance that it&amp;rsquo;s just because you haven&amp;rsquo;t tried it yet: being able to write &lt;a href="https://bauble.studio/">progams that expose scripting interfaces&lt;/a> is a pretty fun superpower.&lt;/p>
&lt;p>Embedding Janet is very easy: the Janet runtime is a small C library, and all you have to do is link it in and then call regular C functions to manipulate Janet values. You can even embed it into &lt;em>websites&lt;/em>, and write &lt;a href="https://toodle.studio/">static sites with custom programmable DSLs&lt;/a>!&lt;/p>
&lt;h1 id="janet-has-mutable-and-immutable-collections">Janet has mutable and immutable collections&lt;/h1>
&lt;p>Janet&amp;rsquo;s collection types come in mutable and immutable flavors. Immutable collections have value semantics: the immutable vector &lt;code>[1 2]&lt;/code> is indistinguishable from &lt;code>(take 2 [1 2 3])&lt;/code>, despite the fact that they have different memory addresses. Mutable collections, on the other hand, have reference semantics: the hash table &lt;code>@{:x 1 :y 2}&lt;/code> is only equal to itself. Another hash table with the same keys and values is a distinct object.&lt;/p>
&lt;p>Not every language has immutable composite values built right into the standard library!&lt;/p>
&lt;h1 id="macros-macros-macros">Macros, macros, macros&lt;/h1>
&lt;p>I think this is the real reason you should learn Janet, but I didn&amp;rsquo;t want to lead with it because I didn&amp;rsquo;t want to scare you off.&lt;/p>
&lt;p>You can write Janet just fine without ever learning how to write macros. But you should learn how, because writing macros is &lt;em>fun&lt;/em>. It feels different than any sort of programming that I&amp;rsquo;ve done before.&lt;/p>
&lt;p>Writing macros requires thinking twice at once: you&amp;rsquo;re writing code to write code, so you have to keep two threads of execution straight in your mind: the code that is running now, at compile time, manipulating values and abstract syntax trees, and the code that you are manipulating, the application code that you produce, the code that will run in the future.&lt;/p>
&lt;p>Janet&amp;rsquo;s macros are not hygienic, and Janet does not have a separate namespace for functions. But by allowing you to unquote literal functions, Janet makes it possible to write macros that are completely referentially transparent. It&amp;rsquo;s an incredibly simple and elegant solution to an &lt;a href="https://ianthehenry.com/posts/janet-game/the-problem-with-macros/">otherwise very delicate problem&lt;/a>. And the fact that it is possible to do this in Janet highlights my next favorite feature&amp;hellip;&lt;/p>
&lt;h1 id="janet-lets-you-pass-values-from-compile-time-to-run-time">Janet lets you pass values from compile-time to run-time&lt;/h1>
&lt;p>This is the most interesting thing about Janet, in my opinion. But it might not sound very interesting at first &amp;ndash; really all it means is that any Janet value can be serialized to disk and read back in later.&lt;/p>
&lt;p>But this serialization is implicit: when you compile a Janet program, it runs all of the top-level instructions &amp;ndash; regular statements, function declarations, whatever &amp;ndash; and then, once it&amp;rsquo;s executed all of the top-level values, Janet writes down a snapshot of your program&amp;rsquo;s state to disk.&lt;/p>
&lt;p>And it&amp;rsquo;s a &lt;em>full&lt;/em> snapshot of your program&amp;rsquo;s state: shared references are preserved, so mutable values can still be mutated after you &amp;ldquo;resume&amp;rdquo; the snapshot. Generators remember exactly what instruction they need to run the next time you resume them. Closures gonna close.&lt;/p>
&lt;p>Macros are a special-case of compile-time code execution &amp;ndash; manipulating abstract syntax trees to create new functions &amp;ndash; but this is a superpower that you can enjoy without any macros at all. Making a game? Reticulate your splines ahead of time! Or embed assets in your final binary by reading files at compile time &amp;ndash; you can perform arbitrary side effects!&lt;/p>
&lt;p>&lt;a href="https://janet.guide/macros-and-metaprogramming/">&lt;em>Janet for Mortals&lt;/em> has an example of using this to autogenerate database bindings based on a SQL schema file&lt;/a> &amp;ndash; a bit of a silly example, but something that would be quite difficult to do in most languages.&lt;/p>
&lt;h1 id="janet-feels-good-in-the-hand">Janet feels good in the hand&lt;/h1>
&lt;p>This is completely subjective, but I love Janet&amp;rsquo;s syntax. It strikes a perfect balance of simplicity, uniformity, and variety.&lt;/p>
&lt;p>It uses pervasive parentheses, but breaks them up with &lt;code>[]&lt;/code> for lists and &lt;code>{}&lt;/code> for tables.&lt;/p>
&lt;p>Mutable literals are always prefixed with &lt;code>@&lt;/code>: &lt;code>@&amp;quot;mutable string&amp;quot;&lt;/code>, &lt;code>{:immutable hash-table}&lt;/code>, etc.&lt;/p>
&lt;p>Anonymous functions are written &lt;code>(fn [x] (+ 1 x))&lt;/code>, but there&amp;rsquo;s a shorthand notation for lifting any expression into a function with &lt;code>|&lt;/code>: &lt;code>|(+ 1 $)&lt;/code>.&lt;/p>
&lt;p>Janet supports &amp;ldquo;splats&amp;rdquo; or &amp;ldquo;spreads&amp;rdquo; with &lt;code>;&lt;/code>: &lt;code>(+ ;args)&lt;/code>.&lt;/p>
&lt;p>String literals can be written with any number of backticks, and closed with the same number of backticks. Escape sequences like &lt;code>\n&lt;/code> don&amp;rsquo;t apply in backtick-quoted strings, so you can create strings with any contents without ever thinking about how to escape them &amp;ndash; all you have to do is wrap them in a sufficient number of backticks.&lt;/p>
&lt;p>Rest parameters use &lt;code>&amp;amp;&lt;/code> instead of &lt;code>.&lt;/code>: &lt;code>(defn foo [first &amp;amp; rest] ...)&lt;/code>.&lt;/p>
&lt;p>Janet doesn&amp;rsquo;t support reader macros, so the syntax itself is fixed. If you know how to read Janet, you can read all Janet programs. Which is not to say you can make sense of them&amp;hellip;&lt;/p>
&lt;h1 id="janet-prefers-comfort-to-tradition">Janet prefers comfort to tradition&lt;/h1>
&lt;p>Janet does not adhere to the ancient customs. &lt;code>CAR&lt;/code> is called &lt;code>first&lt;/code>. &lt;code>PROGN&lt;/code> is called &lt;code>do&lt;/code>. &lt;code>LAMBDA&lt;/code> is &lt;code>fn&lt;/code>, and &lt;code>SETQ&lt;/code> is &lt;code>def&lt;/code>. &lt;code>nil&lt;/code> is not the empty list; it is its own type, and there are first-class Booleans in the language. It eschews &lt;code>EQ&lt;/code>, &lt;code>EQL&lt;/code>, &lt;code>EQUAL&lt;/code>, and &lt;code>EQUALP&lt;/code>. There is nary a linked list in sight.&lt;/p>
&lt;p>This isn&amp;rsquo;t really &lt;em>good&lt;/em> or &lt;em>bad&lt;/em>, but I thought it was worth calling out: if you saw the parentheses and assumed &lt;code>FORMAT&lt;/code> was not far behind, maybe give Janet a second look.&lt;/p></description><pubDate>Wed, 12 Apr 2023 00:00:00 +0000</pubDate><link>https://ianthehenry.com/posts/why-janet/</link><guid isPermaLink="true">https://ianthehenry.com/posts/why-janet/</guid></item></channel></rss>