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 (typically ) 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,
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
reduce to another screen, or a pixel.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 .
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):
How does this relate to ? Well, there’s this researcher Ed Pegg who posted something fascinating in a blog post (archive) around 9 years ago. When you increase by two per “layer” of the Sierpiński Carpet, the total area of the fractal will eventually approach ! (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 sub-screens, then the total recursive area will represent a variation of the Wallis product
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 in order to get an approximation of . The only problem is that Lambda Screen does not support arbitrary area splits of . 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 Sierpiński Carpet:
Now there are three remaining open problems:
The solution to the first problem is quite elegant: A Church numeral is a right-associative -fold composition, while a (single-color) screen is a left-associative -fold composition applied to some color. So, in order to convert between the two, you can left fold the Church numeral itself like so:
and thus creating a screen of arbitrary size becomes trivial: 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:
Then the first element applied to can be mapped to by injecting a rewrite via :
where we ignore the first argument and replace it while we keep the rest of the left-associative application (the other screens) as-is (argument ).
We also need to implement the actual Church numeral logic, such that we always generate sub-screens when splitting a layer with sub-screens. We can do this by giving the initial term an argument , incrementing it two times at every layer, and – before we convert the Church numeral to a screen with sub-screens using the technique shown above – multiply by itself.
All the 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:
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 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 from this and how quickly it converges.
The simplest way is to count the area that you’re drawing black within the renderer, calculate 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 | |
---|---|
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