Comments:"samelog - three little birds"
URL:http://samesake.com/log/2013/01/03/Three-little-birds/
Simple functions can be used in very surprising ways.
With functional programming on the rise more and more of my time has been dedicated to understanding functions. In doing so I've discovered their deep and rich history in practical mathematics, in particular Combinatory Logic and Lambda Calculus.
Lambda calculus is the use of lambdas (functions) for calculation and is defined in terms of simple application and substitution rules.
For an excellent introduction to the lambda calculus I recommend Bret Victor's Alligator Eggs
Combinatory logic is the use of higher order functions to combine functions. A combinator is a function whose input are functions and whose body is defined only in terms of function application of previously defined combinators and parameters, or lack thereof. That is to say, a combinator cannot introduct anything new, it can only combine.
For an introduction to Combinatory Logic you can do no better than Smullyan's To Mock a Mockingbird or any of the various books or posts by @raganwald.
While all of this might sound complex, in the context of our everyday programming it is rather simple, though the implications are profound.
In To Mock a Mockingbird, Smullyan refers to the functions in combinatory logic as birds, and those of us whom wish to understand them as curious birdwatchers. (As we will see, the birds are quite curious themselves). The premise of the metaphor is that you may call out to some bird, and it in turn will call back to you. This is like providing a parameter to some function so it then evaluates its body to return some response. Birds only speak in the language of the birds, so if you call out to them in some foreign tongue they may have unusual responses!
I think this metaphor is marvelous for functional programming since many higher level abstractions don't have suitable names. Below I will show examples of some of these birds, and use them to derive a few famous combinators. In summary, birds are those functions whose parameters are functions which are defined in terms of functions and who often return functions!
Entering the forest
While walking through the combinator forest, the first bird we see is one which is often misjudged. It's called the idiot bird or the identity bird, and when you call out a bird to it it simply calls that bird back to you.
In javascript it would look like this:
// Ix = xfunctionidiot(x){returnx;}; |
This bird seems useless, a function that takes a parameter only to spit it back at us! What an idiot! Some have speculated that the idiot is actually deeply enamoured with other birds and so only calls them back out of appreciation.
The next bird we encounter is the mockingbird. The mockingbird is a mystical bird which has the interesting property of duplicating its input. If you call some bird x to the mockingbird it will call back as if the bird x had called out to itself. In javascript:
// Mx = xxfunctionmockingbird(x){returnx(x);} |
What use is this bird?
It's easy to find useless things we can do with it, for instance if you call out the idiot bird to the mockingbird it will call out the idiot bird back to you. That seems like the behavior of the idiot bird itself, however if you call the mockingbird to the mockingbird it will never stop trying to figure out what it should say about itself!
MI = II = I
MM = MM = MM = MM = ..
Even with this, it's hard to conceptualize a clear use case for this peculiar bird. The mockingbird lies at the heart of combinatory logic. Sometimes called the U combinator, the mockingbird is the primary tool for self application, which is necessary to simulate recursion. Without recursion simple languages like Lambda or Combinator Calculus are not expressive enough for universal computation.
Talking to the mockingbird
If it suits our fancy we can even engineer our own mechanical bird to calculate a factorial. I've heard a rumour that if you call out to a mockingbird you can achieve anonymous recursion. The mental giants who computed before computers spoke to this magical bird chiefly for this purpose, and so in order to better appreciate their work we can try it for ourselves. Our foreign bird doesn't have to follow the rules of regular combinator birds but it will follow some, like being a function which takes in a function and returns a function.
varstrangeFact=function(fact){returnfunction(n){if(n<2)return1;varstrangerFact=mockingbird(fact);returnn*strangerFact(n-1);};};// mockingbird(strangeFact)(23);// mockingbird(strangeFact) is strangeFact(strangeFact) |
Initially we call the strange factorial out to the mockingbird, which calls back the same thing as calling the strange bird to itself. What this does is bind the parameter fact
to the strange bird and return a function who knows how to do this again should it ever need another copy of itself (Line 4).
This works but is very... strange. In modern javascript, we could write less code with fewer dependencies and wind up with something that is easier to use and more performant to boot!
varfactorial=function(n){if(n<2)return1;returnn*factorial(n-1);};// factorial(23); |
While the use of the U combinator style recursion may seem outdated in modern programming parlance, its impact on computer science cannot be understanded. Combinator and Lambda Calculus are much simpler notations than javascript which owes its higher level abstractions to the work of those who spoke to birds like these long before us.
Problems with the mockingbird
As a birdwatcher who hopes to fashion birds of our own using the mockingbird to simulate recursion is awkward at best. We have to know about the mockingbird inside of our function and manually handle the self application at each step along the way, what's more our bird must also be returned from the mockingbird. What we'd like is a way to be flexible enough in our bird creation to not have to worry about this technical detail and be free to focus on only the problem itself. Perhaps there is another bird who can help us achieve what we want.
Bluebirds
The next bird we encounter, the most immediately practical of these strange birds is the bluebird. If you call out some bird x to the bluebird, it'll call back to you with an entirely new bird which knows all about the bird x. This new bird has the curious property that if you call out some bird y to it, it will return some even newer bird that knows about the birds x and y. If you give this final bird some bird z, the bluebirds work will be done and it will return the bird that would have resulted from calling out bird z to bird y, and calling the resulting bird out to the bird x. This is what is referred to as a closure, because each successive function closes over the value of one parameter.
Having trouble picturing it?
// Bxyz = (x·y)z = x(yz)varbluebird=function(x){returnfunction(y){returnfunction(z){returnx(y(z));};};}; |
This is a special curried way to say:
varbluebird=function(x,y,z){returnx(y(z));} |
The difference is that the first definition allows you to define a bluebird with only an x or only an x and y, rather than only with x y and z. This technique is called partial application.
The bluebird is also known in many circles as compose and can be used to thread some value through arbitrary function sequences. Traditionally in combinatory logic the threaded value is a function itself but as we will see it doesn't have to be.
The core of the bluebirds functionality is that it defers the application of x in favor of first applying y.
Here is an example of using the bluebird to construct increasingly complex functions.
varcompose=bluebird,times=function(n){returnfunction(x){returnx*n;};},times6=compose(times(2))(times(3)),invert=times(-1),timesNeg6=compose(invert)(times6);timesNeg6(-6);// 36 |
Here we have times being a closure that returns a function with the value of n closed over. With times6
being a bluebird, we can reduce the above invocation by following the rule of Bxyz = (x·y)z = x(yz)
where x is invert
, y is times6
and z is -6
.
(invert·times6)-6 = invert(times6(-6)) = invert((times2·times3)-6)
invert(times2(times3(-6))) = invert(times2(-18))
invert(-36) = 36
The bluebird allows for some remarkably beautiful chaining, and we've only begun to scratch the surface with this example.
Birds of a feather
Continuing along, the next pair of birds we meet are the strangest yet. They seem to have much more in common with the mockingbird than the bluebird though in actuality they are simply a hybrid of the two. These birds are both dear friends of the mockingbird and in fact are defined in terms of it!
The first of the pair is called the lark. When you call out some birds x and y to the lark, it will respond by calling out the x to the mockingbird of the y.
varlark=function(x){// Mxy = x(My) = x(yy)returnfunction(y){returnx(mockingbird(y));};}; |
Given the lark we can show that LI = M
and LLxy = (Mx)(My)
:
Taking I for x:
LIy = I(My) = I(yy) = yy
Taking L for x:
LLy = L(My)z = (yy)(zz) = (My)(Mz) = (yy)(zz);
The lark is a way to compose some function with self application.
Given this and our knowledge of bluebirds, we could also define the lark as:
varlark=function(x){// Lxy = BxMy = x(My) = x(yy)returnbluebird(x)(mockingbird);}; |
Next is the meadowlark. When you call out some x and y to the meadowlark, it will respond by calling out y to the mockingbird of x
varmeadowlark=function(x){// Mₑxy = Mxy = xxyreturnfunction(y){returnmockingbird(x)(y);};}; |
Note: This bird was unnecessary in combinator calculus because it can also be written as Mxy.
So how can we use the meadowlark?
Given the meadowlark we can show that MₑI = I
:
Taking I for x:
MₑIy = MIy = IIy = Iy = y
Taking Mₑ for x:
MₑMₑy = MMₑy = MₑMₑy = MMₑy = ...
Interestingly the meadowlark behaves much like the mockingbird duplicating forever, with the addition of the deferred application of some bird y.
While interesting, both larks seem to be devoid of any practical use. I would be surprised to see a codebase that could immediately take advantage of these two birds because they solve a particular problem that we don't encounter often in modern programming. These birds are the primary methods of composing self application. (y of the mockingbird of x or x of the mockingbird of y). Why would we want to compose self application? Perhaps the answer lies even deeper in the combinator forest.
Finding Curry's Sage Bird
There are special birds in the combinator forest called Sage birds. Sage birds are particularly useful for finding the fixed points of other birds, where a fixed point is the element when called out to some bird will result in the bird calling the same thing back to you. That is, f(x) = x
. Everything is a fixed point of identity, but finding the fixed points of other birds is much harder to reason about. So why is it useful to have a bird to help you find these invariant points? Well, having a fixed point means you have found the limit of the function, also known as the convergence. In recursive functions, the convergence is of particular importance since it is often the only way to terminate evaluation. Can you see why locating a convergence can be useful if we want to represent recursion abstractly? Consider how strangeFact
uses the mockingbird to advance computation by a step. Can you see how we can use the lark and meadowlark to abstract these steps?
The Y Combinator
Haskell Curry saw this clearly and created the most popular of all sage birds, dubbed the Y combinator.
In the lambda calculus it would look like this:
λf.(λx.f (x x)) (λx.f (x x))
If you look carefully you might notice that there are some familiar friends here, in particular the mockingbird (x)(x)
in the outer form and the lark x(yy)
in the inner form resulting in (x(yy))(x(yy))
. We should be able to define the sage bird in terms of our previous birds, a proper combinator!
First, here's what it would look if we didn't use combinator and translated the lambda terms literally:
varsage=function(f){return(function(x){returnf(x(x));})(function(x){returnf(x(x));});}; |
This is a bit dizzying, and we can simplify it by recognizing that we are asking for the mockingbird of the inner function.
varsage=function(f){returnmockingbird(function(x){returnf(x(x));});}; |
And by realizing that we want the lark of f:
varsage=function(f){returnmockingbird(lark(f));}; |
And by realizing the above is a means of composing f (as bluebirds z):
varsage=bluebird(mockingbird)(lark); |
This is fantastic! We've shown that the famous Y combinator can be represented as the combination of three little birds!
The only problem is that the above code doesn't actually work.
We can show that it is mathematically sound, with Θ
as the sage bird:
Θx = BMLx = M(Lx) = Lx(Lx) = x(Lx(Lx))
The last two reductions can be flipped as x(Lx(Lx)) = Lx(Lx)
which shows that given Lx(Lx)
x will return Lx(Lx)
. The problem is that in modern programming environments, we can expand Lx(Lx)
infinitely.
Lx(Lx) = x(Lx(Lx)) = x(x(Lx(Lx))) = x(x(x(Lx(Lx)))) = ...
This reduction strategy used above is named call by value where the former is referred to as call by name. In call by name systems we can simply call this thing by the name Lx(Lx)
, but in call by value languages (like javascript and most popular languages) we get lost when asking for the value, similar to a mockingbird attempting to mock itself. Maybe we can find a different bird that works in call by value systems.
Introducing the Z combinator
Seeing the shortcomings of the Y combinator, we can see that if we can somehow defer evaluation of the lark we can avoid blowing up the stack. So rather than invoking the lark at sage(x)
we can defer this until we have some z to call it with. This means the z of bluebird should be the actual argument to the function returned by the this new combinator, not the function provided to it. This means we will have to move our bluebird, too!
varsage=function(x){// x = computation step, y = higher order fn// Θx = M(BxMₑ) = M(x·Mₑ) = (x·Mₑ)(x·Mₑ) => (x(Mₑy))(x(Mₑy)) => (x(yy))(x(yy))returnmockingbird(bluebird(x)(meadowlark));}; |
The above combinator is known as the Z Combinator, though sometimes it is called the Y combinator since it reduces to Y. While the Z combinator is a bit more cumbersome and harder to reason about, it properly combines the pieces to work with a call by value system by using the meadowlark to manually defer evaluation.
Having seen this new bird, let's revisit our strange factorial.
varfactorial=function(strangeBird){returnfunction(n){if(n<2)return1;returnn*strangeBird(n-1);};}; |
If we know we are working with a fixed point combinator, we can ignore the strangeBird
and deal directly with the inner function (the actual factorial fn). This means that we no longer have to deal with manual self application thanks to higher order abstractions. Let's see it in action!
varbrokenFact=factorial();// this function isn't tied to the sage bird, it's just a partial functionbrokenFact(1)// properly returns 1brokenFact(2)// blows up since strangeBird is not defined, meaning the only domain this function is defined for is 1 |
The sage bird can help us transform our bird into one that can calculate all the way down to it's fixed point by trampolining the fn which returns its definition.
varsageFactorial=sage(factorial);sageFactorial(1)// 1sageFactorial(12)// 479001600 |
The Z combinator or the mockingbird of the bluebird of x and the meadowlark is a way to forever generate fresh bindings of some function so a copy is available at each step of computation. Unlike our U combinator, the Y and Z combinators also takes care of self application so we don't have to worry about using the mockingbird to rebind our name inside our function. The above birds are achingly clever, and yet built of out relatively simple parts!
Heading home
I hope I've helped elucidate some small corner of the wonderful world of combinators for you, as others have done for me. I've heard it said that if classes are nouns of programming and functions of the verbs then combinators are the adverbs. We've demonstrated how we can use these combinators to factor out common patterns from our functions or as building blocks in and of themselves. Even something as opaque as the infamous Y combinator can be reduced in terms of it's simpler components.
It may surprise you to learn that there are an infinite number of sage birds to be found by combining simple combinators, can you find any others?