Making Sense of Lambda Calculus 3: Truth or Dare With Booleans
By Artyom Bologov
With numbers out of the way, we can finally get to booleans—a simple yet quite atypical topic (at least in its Lambda Calculus rendering). What I'm used to since my university times is truth tables for boolean ops. Lambda Calculus (LC) booleans work in an absolutely different way. Abandon tables ye who enters here.
True and False, the fundamental booleans, are simple: Just take two things and return the first (true) or second (false).
Coincidentally, false
is the same function as 0 and null list (ooops, spoilers!)
Convenient for me as a C programmer used to 0 == NULL == false
.
Booleans are Branching
Numbers are
function compositions.
Akin to that, booleans are computation branching.
Meaning, boolean operations are kind of building trees out of given data.
For example, here's how
not
operator looks like in LC:
So it takes a boolean and applies it to false
and true
.
If the boolean is true
, then it picks the false
branch, and vice versa.
A decision tree of sorts.
Some more elegant branching operations, and
& or
:
Again, branching operations instead of truth tables.
And here's how
XOR
looks like:
Simple and smart at the same time.
Ah, Logic Again
But if we try to define operations like nand
or nor
, our branching doesn't help much.
Here's how
nand
might look like:
Not too bad, but it totally obviates the meaning of the operation.
It's just boolean juggling.
The truth tables I'm trying to avoid.
But the good thing about Lambda Calculus is that we can use the functions from before to fix that.
(Well, at least notation-wise—the compiled lambdas won't have names.
Still, most LC implementations allow for some let
/where
syntax, so I'm safe.)
This way derivative operations look much more meaningful:
We built our primitives out of branching functions. And then used them to define derivative operations. An elegant solution mapping boolean logic directly to LC. No truth tables, which is a win. Now to actually using these operations:
Laconic If
Cutting it short, if
is:
Booleans already select between branches.
So if
doesn't need to do anything: just apply the boolean to both options.
And watch the magic happen.
A person familiar with the regular if
that chooses and runs only one branch:
"But that's dangerous: it's evaluating both branches. There might be side-effects."
There are no side-effects in LC, though.
And there is no "evaluation" in it either:
only reduction.
It's okay.
Applying Booleans to Numbers
That's the part I've been teasing you with in the previous post. Finally, we can do comparisons and predicates on numbers. Here's the most basic—checking for zero:
Numbers are applying a function to a value.
If they do apply it even once (number > 0), return false
.
If they don't (number = 0), return
true
.
That simple.
And it doesn't matter if the number is arbitrarily big.
Applying (λx.false)
has no side-effects (again, there are none in LC).
So we can safely apply it as much as we wish to.
Comparisons, the simplest ones:
The number A is greater than another number B if subtracting B from A yield non-zero. (Notice that Church/Peano numerals are strictly non-negative. Smallest value subtraction can yield is zero.) And vice versa. Having these, other predicates are easy:
Maps the mental model perfectly: number A is greater if it's not less than or equal than B. And equality (this is slightly tricky) is when the numbers are both less than or equal than each other. Because if both are, then both must be the same number. Otherwise one of these will be greater and the condition will be false.
Finally, Using Booleans!
The simplest example for usage of all these operators might be minimum and maximum functions:
I might've used
leq
here, but the behavior stays the same.
It doesn't matter which of the two we pick—both are valid numbers.
If they are the same number, they are fully
β-equivalent
and picking either is fine.
So it does not matter whether there's
leq
or lt
.
Up Next: Consing Up The World
With that, I'm ready to conclude the booleans post. What happened here was:
- Booleans as decision trees.
- Branching logic operations.
- Perfect mapping from logic to LC for derived ops.
- Number predicates.
- And, finally, conditional operations on numbers (or any data, really—the idea is the same.)
With basic data types, numbers and booleans, one can already encode a lot. But there's a point at which one wants aggregate data types. Like cons trees/lists/structures. That's the next post topic, so byte (sic) your nails until then.