This is an implementation of the λ-calculus in Rust. It has a few niceties to
make it more of a usable programming language. As a mathematician/computer
scientist, you should be familiar with the basic syntax: λa.a is the Identity
function (also known as I, „Identitätsfunktion“, “Idiot”). We also support the
shortened syntax, so that λa b.a is the Kestrel (also known as K, C,
„Konstante Funktion“, “Constant Function”). It is transparently π-expanded to
λa.λb.a by the parser. Additionally, you can assign variables: I ← λa.a,
😈 ⇐ (λf.ff)(λf.ff). As you can see, 😈 is initialized using the expression
(λf.ff)(λf.ff) instead of (λf.f f)(λf.f f), which is because ⇐ activates
the single-letter-form (aka. math-form).
But how do you run programs using this notation?
That’s pretty simple: You mutate it, in different ways. The normal computations are done using β-reduction, but the other procedures are also important.
This is probably the most complicated algorithm as there is no obvious approach.
Take the identity function λa.a. It can also be expressed as λb.b, λc.c,
λα.α, λÄ.Ä, λᴍʏᴠᴀʀɪᴀʙʟᴇ.ᴍʏᴠᴀʀɪᴀʙʟᴇ, λ🏳️⚧️.🏳️⚧️, or any other way to
replace a everywhere in the function. This works as long as our new symbol
doesn't appear freely in our original function. For example, α-renaming a
function λa.λb.a to λa.λa.a is wrong, because a is free in λb.a.
This might seem simple, but, as I already said, it isn't. The difficult part is determining, where to α-rename to which variable names. That has no standard solution.
The most important part.
Take the term (λa.a)(λb.b). It being β-reduced is commonly written as
(a)[a := (λb.b)], which results in λb.b.
In a general way, the term (λx.f x)y is β-reduced to f y.
To give you another example, let's add one and one:
+ 1 1 = (λm n f x . m f (n f x))(λf x.f x)(λf x.f x)
→ (λn f x . (λf x.f x) f (n f x))(λf x.f x)
→ λf x . (λf x.f x) f ((λf x.f x) f x)
→ λf x . (λx.f x) ((λx.f x) x)
→ λf x . (λx.f x) (f x)
→ λf x . f (f x) = 2
This is quite simple, you might also know it as “point-free programming”.
A function λx.f x can be written as f. That's it!
A real world example: You want a function for adding two. The obvious solution
would be λ x . + 2 x. But if you want to feel like a real badass hacker,
you can write it as + 2.
Most Haskell linters even force you to write your code this way, and you should.
This one implements unsigned integers aka natural numbers.
When β-reducing, integers are automatically ι-expanded like this:
num = Σ("x")
while i > 0:
num = Α(Σ("f"), num)
i--
return Λ("f", Λ("x", num))
This gives you the correct Church encodings for all unsigned integers:
- 0 →
λf x.x - 1 →
λf x.f x - 2 →
λf x.f (f x) - …
Here, functions with multiple parameters are converted into proper λ-calculus.
For example, the function λa b.a is expanded into λa.λb.a.
It works like this:
func = body
for param in params.reverse():
func = Λ(param, func)
return func
We use pest to parse your statements into a high-level AST, then we generate
proper ASTs from that. Those basically look like that:
λa b.a b η-reduces to λa.a.
Here's how to add 1 and 1 using β-reduction: