Pure lambda calculus has encodings for many different data
structures like lists, numbers, strings, and trees. Wrapped in monadic IO, lambda calculus
provides a great interface for computation – as can be seen in
user-friendly syntactic variants like my programming language bruijn. Such simple languages,
however, typically don’t support graphical output (aside from
ASCII art).
I present the “Lambda Screen”, a way to use terms of pure lambda
calculus for generating images. I also show how recursive terms (induced
by fixed-point combinators) can lead to infinitely detailed fractals.
If you want to skip the details or want to figure out its inner
workings yourself, go to my reference
implementation (source-code)
and flip through the examples.
Encoding
A screen is a term
λx.(xtltrblbr),
where the terms tl, tr, bl, and
br represent the top-left, top-right, bottom-left, and
bottom-right quadrants of the image. Each of these terms can either be
another screen, or a pixel.
A pixel is either on (white) or off
(black). In its on state, a pixel is defined as the
k combinator
w=λwb.w
In its off state, it’s the ki combinator
b=λwb.b
This decision was made for simplicity and could be any other state
encoding, including ones of arbitrary size and color.
Example
Click the “Render” button to see the results.
λ_x.(xbbbb)
λ_x.(xλy.(ybwwb)wwb)
Rendering
For this project, I decided that the entire behavior of the screen is
defined by a single closed lambda term. Upon execution of the renderer,
this function gets applied to the default empty screen (four white
squares). This enables the use of point-free/Tacit programming
(e.g. using combinators), such that you don’t necessarily have to
construct screens/colors at all.
Through slightly modified beta-reduction, the screen gets updated
in-place while the term converges to its normal form. High rendering
depths or diverging behavior may stop the renderer before convergence is
reached.
Note that the renderer would also work with normal beta-reduction
until convergence. The additional steps were only made to add support
for in-place rendering of diverging terms.
Reduction
The in-place reduction-rendering works as follows (written as
pseudo-Haskell using de Bruijn indices; actually implemented in
JavaScript).
nf (Abs t) =Abs$ nf tnf (App l r) =case nf l ofAbs t -> nf $ subst t r t ->App t (nf r)nf t = t
Reduce to weak-head normal form:
whnf (App l r) =case whnf l ofAbs t -> whnf $ subst t r t ->App t rwhnf t = t
Reduce to screen normal form (either
λx.((((xtl)tr)bl)br),
λwb.w,
or
λwb.b):
snf t =case whnf t ofAbs b ->caseAbs$ whnf b of t@(Abs (Abs _)) -> nf t -- not a screen! t ->let go t | isScreen t = t go (App l r) =case whnf l of (Abs t) -> go $ subst t r t -> go $App t (whnf r) go (Abs t) = go $Abs$ whnf tin go t _ ->error"not a screen/pixel"
Main reduction and rendering loop (assuming drawing functions that
use and return some ctx):
Here, the rendering is stopped automatically after the smallest
resolution of the canvas is reached. Note how the triangle structure
appears even though we only ever explicitly draw black pixels.
This is because the unresolved recursive
triangle
calls get (temporarily) drawn as grey until they also get reduced. If
the reduction wouldn’t be stopped, the canvas would slowly become black
entirely.
T-Square
In each iteration, the T-Square
adds new overlapping squares to the quadrants of every square already
drawn. For lambda screen we can interpret this as a recursive
split into smaller squares, where in each iteration one of the quadrants
(depending on its position) gets drawn as black.
We can model this iteration as a mutual recurrence
relation:
Thus giving us the final term
tsquare′,
where the indices indicate the
nth
head selection of Church lists:
tsquare′=λ_x.(xmutual0mutual1mutual2mutual3)
Simplification
This section was added 6 months after publishing.
It turns out that the variadic fixed-point combinator is very open to
optimization when choosing a different list encoding and fixed adicity
(like here, 4).
By using
n-tuples
instead of nested Church pairs and additionally giving a
mapping argument
m,
the variadic fixed-point combinator can be directly replaced with a
normal fixed-point combinator such as
y:
tsquare′′=λ_.(yλms.(s(mtl′)(mtr′)(mbl′)(mbr′)))
This elimination of explicit mapping only works because the screens
themself are also
4-tuples.
“Sierpiński Carpet”
The exact drawing of the Sierpiński
Carpet is left as an exercise to the reader.
Since it’s a bit more straightforward, I decided to generate a slight
variant1 where each quadrant isn’t split into
9 but 4 quadrants per iteration.
Each new stable quadrant will be one of the following:
The previously explained simplification can be applied here as
well.
Cantor Dust
The Cantor
dust is another great example of how infinitely detailed structures
appear as unreduced (grey) quadrants when reduction inevitably gets
halted.
In this case, a square gets recursively split into four new squares.
Since we do the same split four times, we can construct the new quadrant
using a
quadruple
function: