Squeezing π\pi from 122 Bits

Marvin Borner

2025-02-09

Recently I had some excess of procrastination potential (exam phase), so I finally got around to a fun idea I had many months ago. The following binary term calculates Pi to infinite precision:

00010100011010000000010
11100111000011001011111
   01111       00000
   01110       01110
   01011       11110
   11010       00000
   10111       10000
  01010        000001110
 01110          0111010

…well, almost, let me explain :)

There’s this fractal called a Sierpiński Carpet (also known as its 3D variant, the Menger sponge). It works by dividing some area into nn (typically n=9n=9) parts, and repeating this step on every subarea except the one in the center. Doing this infinite times will result in a fractal.

There’s also this concept called Lambda Screen that I’ve created. It can render fractals and other images using a recursive, quad-tree-like, purely functional product type. Specifically,

Rendering of terms will stop at a certain depth by drawing the unreduced terms as grey in the current screen context. The outermost term also gets an empty screen as additional argument to allow a kind of monadic IO – we ignore this here using λ_\lambda\_.

By strategic use of fixed-point recursion, you can write some beautifully elegant terms such as this 51-bit term (in Binary Lambda Calculus (BLC), currently the smallest in a codegolf challenge):

λ_.(λf x.(x f b f f))\lambda\_.(\textrm{y}\ \lambda f\ x.(x\ f\ \textrm{b}\ f\ f))

How does this relate to π\pi? Well, there’s this researcher Ed Pegg who posted something fascinating in a blog post (archive) around 9 years ago. When you increase nn by two per “layer” of the Sierpiński Carpet, the total area of the fractal will eventually approach π4\frac{\pi}4! (no, not factorial)

This might seem counterintuitive at first, but it actually makes perfect sense. When splitting a screen and recursing it on itself in each of its nn sub-screens, then the total recursive area will represent a variation of the Wallis product

8924254849120121=π4.\frac{8}{9}\cdot\frac{24}{25}\cdot\frac{48}{49}\cdot\frac{120}{121}\cdot\dots=\frac{\pi}4.

So, if I can write a minimal lambda term that generates such fractal, we will only have to measure the recursive (grey) area that the Lambda Screen renderer draws and multiply it by 44 in order to get an approximation of π\pi. The only problem is that Lambda Screen does not support arbitrary area splits of n4n\ne4. This was a simple change in the renderer though that doesn’t even affect the behavior of existing quad-tree terms:

This allows us to draw the following n=9n=9 Sierpiński Carpet:

λ_.(λf x.(x f f f f b f f f f))\lambda\_.(\textrm{y}\ \lambda f\ x.(x\ f\ f\ f\ f\ \textrm{b}\ f\ f\ f\ f))

Now there are three remaining open problems:

  1. How do we generate nn squares from a numeric encoding?
  2. How do we exclude the square in the center from the recursion?
  3. And how do we pack all of this into a teeny-tiny lambda term?!

The solution to the first problem is quite elegant: A Church numeral is a right-associative nn-fold composition, while a (single-color) screen is a left-associative nn-fold composition applied to some color. So, in order to convert between the two, you can left fold the Church numeral itself like so:

λf.(λx.(xxn) λx.(x f))λf x.(x f  fn)\lambda f.(\lambda x.(\overbrace{x\circ\dots\circ x}^{\mathclap{n}})\ \lambda x.(x\ f))\leadsto\lambda f\ x.(x\ \overbrace{f\ \dots\ f}^{\mathclap{n}}) and thus creating a screen of arbitrary size becomes trivial: λf.(λx.(xxn) λx.(x f) b)λx.(x  bn)\lambda f.(\lambda x.(\overbrace{x\circ\dots\circ x}^{\mathclap{n}})\ \lambda x.(x\ f)\ \textrm{b})\leadsto\lambda x.(x\ \overbrace{\textrm{b}\ \dots\ \textrm{b}}^{\mathclap{n}}) In our case, the screen will of course contain itself recursively instead of a single color, but the difference doesn’t matter here – it’s still the same argument.

* * *

Since taking the center element of such a functional structure is difficult and ugly, the second problem requires a little trick: We don’t actually care that the non-recursive term in each layer is precisely at the center – as long as it’s always exactly a single term. So, instead we map the first element of the structure we previously generated to a fixed color while keeping the rest.

We first eta-expand the screen term:

λf x.(x f  f)λf x.(λf x.(x f  f) f x)\lambda f\ x.(x\ f\ \dots\ f)\htmlClass{katex-flip-x}{\leadsto}\lambda f'\ x'.(\lambda f\ x.(x\ f\ \dots\ f)\ f'\ x')

Then the first element applied to xx can be mapped to b\textrm{b} by injecting a rewrite via xx':

λf x.(λf x.(x f  f) f λh t.(x b t))\lambda f'\ x'.(\lambda f\ x.(x\ f\ \dots\ f)\ f'\ \lambda h\ t.(x'\ \textrm{b}\ t))

where we ignore the first argument hh and replace it while we keep the rest of the left-associative application (the other screens) as-is (argument tt).

We also need to implement the actual Church numeral logic, such that we always generate (n+2)2(n+2)^2 sub-screens when splitting a layer with nn sub-screens. We can do this by giving the initial term an argument nn, incrementing it two times at every layer, and – before we convert the Church numeral to a screen with nn sub-screens using the technique shown above – multiply nn by itself.

All the nn ff above will need to be mapped to the term itself – only that it will now take the incremented Church numeral as argument. All of this comes together in a single term:

λ_.(λf n x.(λf.(nn2 λx.(x f)) (f (inc (inc n))next n) λh t.(x b t)) 3initial n)\lambda\_.(\textrm{y}\ \lambda f\ n\ x.(\lambda f'.(\underbrace{\textrm{2}\ n}_{n^2}\ \lambda x.(x\ f'))\ (f\ \underbrace{(\textrm{inc}\ (\textrm{inc}\ n))}_{\mathrm{next}\ n})\ \lambda h\ t.(x\ \textrm{b}\ t))\ \underbrace{\textrm{3}}_{\mathclap{\mathrm{initial}\ n}})

* * *

Now onto the final problem – making the term tiny! This is done using incredibly complex and highly confidential techniques (i.e. carefully beta-reducing the non-recursive parts with some off-the-shelf strong reducer) and results in the following final term (in bruijn notation):

π [[0 0] [[[1 (1 [0 (3 3 [[1 (1 (4 1 0))]])]) [[2 [[0]] 0]]]]] (+3u)]

Converting the term to BLC will result in the 122-bit program from the beginning. Unfortunately I was not able to golf the Church three into the inner term, so the initialization number alone takes 17.5%17.5\% of the size!

Here’s the rendering of the term (this may crash your tab if you’re using a phone, chrome, safari, or something else that I don’t use)

While this doesn’t look like an incremented Sierpiński Carpet anymore, it’s actually fully equivalent to a quadrant of Ed Pegg’s design – just with the stable, non-recursive area at the top left.

* * *

So you probably also want to know how to get a concrete approximation of π\pi from this and how quickly it converges.

The simplest way is to count the area that you’re drawing black within the renderer, calculate (1area)(1-\mathrm{area}) to get the recursive area (grey), and multiply this number by four.1 This way you will get the following approximations, based on how many layers deep you go:

layers π\approx\pi
1 3.55555
2 3.41333
3 3.34367
4 3.30239
5 3.27510
6 3.25572
7 3.24125
8 3.23003
9 3.22108
10 3.21378
11 3.20770
12 3.20257
13 3.19818
14 3.19438
15 3.19105
16 3.18812
17 3.18552
18 3.18319
19 3.18110
20 3.17921
21 3.17749
22 3.17592
23 3.17448
24 3.17316
25 3.17194
26 3.17081
27 3.16976
28 3.16879
29 3.16788
30 3.16702
31 3.16623
32 3.16548
33 3.16477
34 3.16411
35 3.16348
36 3.16289
37 3.16232
38 3.16179
39 3.16128
40 3.16080
41 3.16034
42 3.15991
43 3.15949
44 3.15909
45 3.15871
46 3.15834
47 3.15799
48 3.15766
49 3.15733
50 3.15703
51 3.15673
52 3.15644
53 3.15617
54 3.15590
55 3.15564
56 3.15540
57 3.15516
58 3.15493
59 3.15470
60 3.15449
61 3.15428
62 3.15408
63 3.15388
64 3.15369
65 3.15351
66 3.15333
67 3.15316
68 3.15299
69 3.15283
70 3.15267
71 3.15251
72 3.15236
73 3.15222
74 3.15208
75 3.15194
76 3.15180
77 3.15167
78 3.15155
79 3.15142
80 3.15130
81 3.15118
82 3.15106
83 3.15095
84 3.15084
85 3.15073
86 3.15063
87 3.15053
88 3.15042
89 3.15033
90 3.15023
91 3.15014
92 3.15004
93 3.14995
94 3.14987
95 3.14978
96 3.14969
97 3.14961
98 3.14953
99 3.14945
100 3.14937

As you can see, this converges quite slowly. Notably, I’ve never gotten deeper than layer 5 using my JavaScript implementation of Lambda Screen, so you probably won’t get some new world record approximation using this technique. You can see the measurement of the rendering above in your browser’s console, it will probably be around 3.302 (layer 4).

I’ve also implemented a Haskell higher-order reducer for this specific type of problem which counts the total area drawn for any term that uses a perfect square amount of sub-screens. I did some experiments with parallel reduction, but it also does not get much further than the JavaScript renderer.

One problem is that at every level we have to “lower” down the HOAS encoding in order to count the area and continue on all sub-screens – I think this might impact sharing of reductions.

This also leads me to a challenge I have for you! Please let me know if you come up with a reducer that measures the area of such fractals more efficiently. Due to its inherent parallelism and sharing of redexes I believe that optimal reduction could be the optimal approach to this problem. My experiments with the HVM optimal runtime failed though, since the term constantly duplicates non-affine terms.

Also let me know if you find smaller encodings of the fractal (and π in general!), I’m fairly certain that it could be golfed below 100 bit.

You can find the Haskell program in a GitHub Gist – have fun!

Contact me via email. Support on Ko-fi. Subscribe on RSS. Follow on Mastodon.


Imprint · AI Statement · RSS