Open stopachka opened 4 years ago
Big fan of your blog; I have to say I've enjoyed every blog post I've read so far -- each one is filled with insight and is very fun.
When I was reading this one, I noticed that you wrote this:
What if “numbers”, where higher-order functions with two arguments: a function f, and a value v...
I think you meant something like this (where -> were):
What if “numbers” were higher-order functions with two arguments: a function f, and a value v...
Anyway, keep up the good work, I've been working through the code for the risp blog and the "intuition for lisp syntax" through javascript and they're both chock full of insights :)
Brought a big smile to read this, thank you for the kind words and the catch Takashi! Updated :)
If you're ever in NYC let's grab coffee!
I would love to!
In 1935, a gentleman called Alonzo Church came up with a simple scheme that could compute…just about anything. His scheme was called Lambda Calculus. It was a phenomenal innovation, given that there weren’t even computers for him to test out his ideas. Even cooler is that those very ideas affect us today: anytime you use a function, you owe a hat tip to Mr. Church.
Lambda Calculus is so cool that many hackers use it as their secret handshake — a “discreet signal” if you will. The most famous, of course, is PG’s Y Combinator. In this essay, we’ll find out what it’s all about, and do things with functions that we’d never have imagined. In the end you’ll have built just about every programming concept: numbers, booleans, you name it…just with functions.
0: Intuition with Pairs
City dwellers who drive SUVs rarely consider their cars as ferocious machines that traverse rocky deserts and flooded rivers. It’s the same with programmers and functions. Here’s what we think functions do:
Safe, clean, and useful. We’re so accustomed that it would surprise us to find the myriad of ways we can bend functions to do just about anything.
Let’s step out into the wilderness a bit. Say you wanted to make a data structure for pairs:
How would you do it? It’s sensible to use a map or a class or a record to represent a pair. But…you could use functions too.
Here’s one way we can make a pair:
No maps or classes…it just returns a function!
Now our
ex-pair
takes aselector
argument. What if we ran ex-pair with this selector:Well,
(ex-pair (fn [a b] a))
would expand too:Which would return…
a
!That just gave us the
first
value of our pair! We can use that to write achurch-first
function:And do something similar for second:
We just used functions to represent pairs. Now, since the grammar for Lisp is just a bunch of pairs plopped together, that also means we can represent the grammar of Lisp…with just functions!
1: Factorial
What we just did was analogous to a city dweller driving their SUV…on a snowy day. It gets a lot crazier.
We said we could represent everything. Let’s go ahead and try it!
Here’s what can do. Let’s take a function we know and love, and implement it from top-to-bottom in Lambda Calculus.
Here’s factorial:
By the end of this essay, we’ll have built factorial, only with functions.
2: Rules
To do this, I want to come up front and say I am cheating a little bit. In Church’s Lambda Calculus, there is no
def
, and all functions take one argument. Here’s all he says:In his rules, you define anonymous functions by popping a little
λ
in front. What follows is the argument, following by a.
.After the.
is the application.This is very much akin to a single-argument anonymous function in Clojure:
λ x. x
=>(fn [x] x)
We could follow those rules, but writing factorial like that is going to get hard to reason about very quickly. Let’s tweak the rules just a little bit. The changes won’t affect the essence of Lambda Calculus but will make it easier for us to think about our code. Here it goes:
1) for a single argument function,
(fn [x] x)
maps pretty well to Church’s encoding. We can go ahead and use it as is.2) Since Church’s lambdas only take one argument, For him to express a function with two arguments, he has to write two anonymous functions:
This would map to:
But, nesting our functions like this can get annoying in Clojure [^1]. To make life easier for us, we’ll allow for multi-argument functions:
3) Finally, Church has no concepts of variables outside of what’s provided by a function definition.
For him to express
He would have to “unwrap”
make-pair
To keep our code sane, we’ll allow for
def
, but with one rule:You can use
def
, as long as you can “replace” it with an anonymous function and nothing breaks.For example, imagine if
make-pair
referenced itself:This would break because if we replaced
(def make-pair …)
with an anonymous function, there would be no variable calledmake-pair
anymore!That’s it, these are our rules. With that, we’re ready to make factorial!
3: Numerals
The first thing we need is the concept of a number. How can we do that?
Church thought of a pretty cool idea. What if “numbers”, were higher-order functions with two arguments: a function
f
, and a valuev
.We can figure out what number each function represents by “counting” the number of times
f
was composed.For example, 0 would compose
f
zero times: it would just returnv
. 1, would compose f once:(f v)
. 2 would compose twice:(f (f v))
, and so on.To help us see these numbers in our REPL, let’s create a quick converter function:
Since a church numeral composes
f
the number of times it is called withv
as the first argument, all we need to see what number it is in Clojure, is to provideinc
asf
and0
asv
! Now2
would do(inc (inc 0))
for example, and get us the corresponding Clojure number.Cool!
4: Inc
Take a look at how we wrote two:
What we did here, is delegate f’s composition to the numeral before (in this case
one
), and then just calledf
one more time.What if we abstracted the
one
out?Voila. Give this function a numeral, and it will return a new numeral that calls
f
one more time. We’ve just discoveredinc
!Cool.
Now that we have this function, we can also write a quick helper to translate Clojure numbers to these numbers:
That’ll come in handy for our REPL.
5: Dec: Intuition
Next up, we need a way to “decrement” a number. Well, with
inc
we create a numeral that composesf
one more time. If we can make some kind of function that composesf
one less time, then we’d havedec
!To do that, we’ll need to go on a short diversion.
6: Dec: shift-and-inc
Remember our
pair
data structure? Let’s create a function for it (we’ll use this in just a moment below):shift-and-inc
. All it would do, is take pair of numbers, and “shift” the pair forward by one:For example, applying
shift-and-inc
to(0 1)
, would produce(1 2)
. One more time, it would produce(2 3)
, and so on.Sounds simple enough:
Bam, we take a pair. The second item is shifted over to the first positions and is replaced with its
inc
ed friend. Let’s try it out:Works like a charm!
7: Dec: putting it together
Now that we have
shift-and-inc
, what if we did this:Remember that our
church-numeral
would callshift-and-inc
N times, representing its numeral value. If we started with a pair(0, 0)
, then what would the result be, if we composedshift-and-inc
N
times?Our result would be the pair
(N-1, N)
. This means that if we take the first part of our pair, we havedec
!Nice.
8: Multiplication
Next up, multiplication. Say we multiply
a
byb
. We’d need to produce a church numeral that composesf
,a * b
times. To do that, we can leverage the following idea:Say we made a function
g
, which composesf
b times. If we fed that function toa
, it would callg
, a times.If
a
was “2” and “b” was 3, how many times wouldf
get composed? Well,g
would be composed twice. Each timeg
is composed,f
is composed 3 times. That comes out to a total of 6 times!Bam, if we did that, it would represent multiplication.
Here,
(partial num-b f)
represents ourg
function.Works like a charm!
9: Booleans
We’ve got numbers, we’ve got
*
and we’ve gotdec
. Next up…booleans!To do this, we need to be creative about what
true
andfalse
is.Let’s say this. Booleans are two argument functions:
They take a “true” case and a “false” case. Our
church-true
function would return the true case, andchurch-false
function would return the false case.That’s it. Surprisingly this is enough to handle booleans. Here’s how we could convert them to Clojure bools.
Our
church-true
would return the first argument (true), and ourchurch-false
would return the second one!Do they look familiar? Those are our
selector
functions forchurch-first
andchurch-second
! We could interchange them if we wished 😮10: if
If you are like me, you were a bit suspicious of those booleans. Let’s put them to use and quiet our fears. Here’s how could create an
if
construct:All we do to make
if
, is to simply shuffle things around and provide thewhen-true
andwhen-false
cases to our boolean!church-true
would return thewhen-true
case, andchurch-false
would return thewhen-false
case.That would make
if
work pretty well:11: zero?
We have almost all the constructs we need to implement
factorial
. One missing piece:zero?
. We need a way to tell when a numeral is zero.The key trick is to remember that the
zero
numeral never callsf
.We can use that to our advantage, and create a
zero?
predicate like this:If a number is greater than zero,
f
would be called, which would replacev
withchurch-false
. Otherwise, we’d return the initial value ofv
,church-true
.Wow…I think we’re ready?
12: factorial-v0
Let’s look at
factorial-clj
again:Well, we have
numerals
, we haveif
, we havezero?
we have*
, we havedec
. We could translate this:Wow. That follows our recipe pretty much to a key.
The only weird thing is that we wrapped the
when-true
andwhen-false
cases in an anonymous function. This is because ourchurch-if
is a little different than Clojure’sif
. Clojure’s if only evaluates one of thewhen-true
andwhen-false
cases. Ours evaluates both cases, which triggers an infinite recursion. We avoid this by wrapping both cases in a lambda, which “delays” the evaluation for us. [^2]Will it work?
Wow! 🤯 We did it
13: Broken rules
Okay, almost. We cheated. Remember our
Rule 3
: If we replace our variables with an anonymous function, everything should work well. What would happen if we wrotefactorial-v0
as an anonymous function?Dohp.
factorial-v0
would not be defined.Here’s one way we can fix it. We could update this so
factorial
is provided as an argument to itself.That would work, but we only punt the problem down. What the heck would
????
be? We need some way to pass a reference offactorial
to itself!14: Y-Combinator: Writing it out
Let’s see if we can do make this work. First, let’s write our factorial, that accepts some kind of “injectable” version of itself:
If we can somehow provide that
factorial-cb
, we’d be golden.To do that, let’s create a
make-recursable
function, which accepts thisinjectable-f
Okay, all we did now is move the problem into this
make-recursable
function 😅. Bear with me.Let’s imagine what the solution would need to look like. We’d want to call
injectable-f
with somefactorial-cb
function handles the “next call”.That seems right. Note the comment
recursion-handler
. This is in reference to this form:If we somehow had access to this form, we can use that in
????
! Well, let’s punt the problem down again:Here, we wrap our
recursion-handler
into a function. If it could get a copy of itself, we’d be golden. But that means we’re back to the same problem: how could we giverecursion-handler
a copy of itself? Here’s one idea:Oh ma god. What did we just do?
15: Y-Combinator: Thinking it through
Let’s walk through what happens:
The first time we called:
this would run
recursion-handler
would be:And
recursion-handler
would call itself:So now, this function would run:
And this function’s
recursion-handler
argument would be…a reference to itself!🔥🤯. Oh boy. Let’s continue on.
Now this would run:
injectable-factorial
would be called, and it’sfactorial-cb
function would be this callback:Whenever
factorial-cb
gets called with a new argument,This would end up producing a new
factorial
function that had afactorial-cb
. Then we would call that withnext-arg
, and keep the party going!Hard to believe. Let’s see if it works:
Very cool!
This
make-recursable
function is also called the Y Combinator. You may have heard a lot of stuff about it, and this example may be hard to follow. If you want to learn more, I recommend Jim’s keynote.16: Just Functions
Wow, we did it. We just wrote
factorial
, and all we used were anonymous functions. To prove the point, let’s remove some of our rules. Here’s how our code would end up looking without any variable definitions:😮
Fin
Well, we just took our functions through the Mojave desert! We made numbers, booleans, arithmetic, and recursion…all from anonymous functions. I hope you had fun! If you’d like to see the code in full, take a look at the GH repo.
Bonus: Fun with Macros
I’ll leave with you with some Clojure macro fun. When the time came to “replace” all our
defs
with anonymous functions, how did we do it?In wimpier languages we might have needed to do some manual copy pastin [^3]. In lisp, we can use macros.
First, let’s rewrite
def
. This version will “store” the source code of everydef
as metadata:Then, we can create an
unwrap
function, that recursively replaces alldef
symbols with with their corresponding source code:Aand…voila:
To learn about what’s going on there, check out Macros by Example
Thanks to Alex Reichert, Daniel Woelfel, Sean Grove, Irakli Safareli, Alex Kotliarskyi, Davit Magaltadze, Joe Averbukh for reviewing drafts of this essay
[^1]: Haskell for example treats every function in the same way as Mr. Church!
[^2]: This chapter in SICP may be a good place to get a deeper sense on what the issue is all about.
[^3]: I admit I first just did some copy-pastin. Only after I changed the code a few times did I decide to use the macros xD