But Y would I want to do a thing like this?
Choose life. Choose a job. Choose a starter home. Choose dental insurance, leisure wear and matching luggage. Choose your future. But Y would I want to do a thing like that?
Writing about
first-class functions and their
compatibility with object-oriented programming naturally leads to the
Y combinator. And that is the point where eyes glaze over and soft, snoring sounds rise from RSS readers everywhere.
But please bear with me, this essay is not really about the Y combinator, it’s about learning new things and expanding our capacity to think.
Sharpening the sawYears ago I picked up Steven Covey’s book
The 7 Habits of Highly Effective People. If the book is a test out of seven, I really wasn’t doing very well.
If you’ve read the book, you probably remember that he talked about “Sharpening the Saw,” investing in your own abilities. That’s incredibly important, but I don’t need to tell you that. If you exercise with programming katas, or learn a new programming language once a year, or pick up a book like
The Reasoned Schemer and actually go through the exercises, then you are already in the top 1% of software developers for personal skills improvement. (Sorry, certifications
don’t count. They are the classic case of doing the wrong thing for the wrong reason!)
New ideas—by which I mean, new to you—are an important way to sharpen your saw. If for no other reason than this: the brain needs regular exercise to perform at or near its potential. Learning new things keeps you sharp, even if you don’t directly use the things you learned.
Others have suggested that learning Lisp is beneficial to your programming skills in its own right. That’s one good way to sharpen your saw. But I add to that an important caveat: to obtain the deepest benefit from learning a new language, you must learn to think in the new language, not just learn to translate your favourite programming language syntax and idioms into it.
Think differentThe interesting thing about that is that almost by definition, if you see something in, say, Lisp that solves a problem you already have, you won’t learn much from the Lisp code. It is tempting to think that Lisp (or any other language) will somehow do what you’re already doing in some wonderfully magic way that is obviously better. But no, that isn’t how it really works.
For your problems are tuned to your existing tools. You simply can’t imagine problems that your tools can’t solve well, much less can’t solve at all. That’s why there are so few continuation-based web servers. Who’s going to invent one unless they have a programming paradigm with continuations?
And worse, when a new tool is applied to a problem you think you know well, you will probably dismiss the things the new tool does well. Look at how many people dismiss brevity of code. Note that all of the people ignore the statistics about the constant ratio between bugs and lines of code use verbose languages. Look at how many people dismiss continuation-based servers as a design approach. Note that all of them use programming languages bereft of control flow abstractions.
Thus, to truly learn a new tool, you must not just learn the new tool, you must
apply it to new kinds of problems. It’s no good learning to replace simple iteration with first-class functions: all you’ve learned is syntax. To really learn first-class functions, you must seek out problems that aren’t easily solved with iteration.
The Why of YWhich leads me back to fixed point combinators. They appear to have no practical (as in making money) use. And that’s why I’m suggesting to you that you figure out how to make one in your language of choice. The very fact that the problem is far outside of your realm of “practicality” guarantees that you will learn something. You won’t be simply applying your same-old, same-old techniques and patterns to a slightly new problem.
Start your research with Richard P. Gabriel’s
The Why of Y. Try porting his examples directly to your favourite programming language. If what you want to use is too brain-damaged to support closures, you may need to do a little greenspunning and build a little
functor infrastructure.
Don’t be dissuaded if you have to follow the functor route: you are learning far more about your language and about programming in general than the shmoes that settle for learning five new buzzwords related to the latest WS-* interoperability with XPath 3.x.
If you prefer a
fun approach to learning, you can do not better than Raymond Smullyan’s
To Mock a Mockingbird: an enjoyable romp through the world of combinatory logic. After reading this book, you will have mastered the S, K, I, Y, and other combinators. Added bonuses include a safe that can only be opened by applying Gödel’s Incompleteness Theorem to its combination. How can you read this book and
not learn?
Eating my own dog foodI thought of a few things to say along these lines last week and then I abruptly realizing I was asking you to “Do as I say, not as I do.” What good is recycling problems I first encountered in University textbooks two decades ago? I put this post aside and set to work on a problem of my own.
I set out to write a function for making recursive functions—a function
extensionally equal to the Y combinator—in Ruby. The ultimate goal is to take something like:
lambda { |f| lambda { |n| n.zero? && 1 or f.call(n-1) } }
And be able to have it call itself recursively. In this case, to compute the
factorial
function.
This is trivial, given that Ruby supports named recursion, but if you want to write a fixed-point combinator you want to write a function that makes recursive functions
without using the host language’s support for named recursive calls. In other words, you are bootstrapping named recursion out of anonymous first-class functions.
1There are important theoretical implications of being able to do this, but the killer reason to try it is to learn.
I started my quest for a function for making recursive functions with a rather trivial observation based on OO programming and the Curry function:
require 'test/unit'
class ExempliGratia < Test::Unit::TestCase
CURRY = lambda { |f, a| lambda { |*b| f.call(a, *b) } }
def test_recursive_curry
maker = lambda { |func_with_me|
CURRY.call(func_with_me, func_with_me)
}
assert_equal(120, maker.call(lambda { |me, n| n.zero? && 1 or n * me.call(me, n-1) }).call(5))
end
end
In OO some language implementations,
this
(or
self
) is a hidden parameter passed to each method. Thus, there’s a parameter—
me
in the example code—that is added for handling recursion. If you write a recursive function—like the venerable
factorial
—with the extra
me
parameter, a trivial currying operation evaluates it recursively without any need for names.
This is obviously deficient. As noted above, we want to write
factorial
like so:
lambda { |n| n.zero? && 1 or f.call(n-1) }
We’ll need an
f
from somewhere, and just as our Scheme colleagues do, we’ll bind one as a parameter in an enclosing lambda. So we want to write:
lambda { |f| lambda { |n| n.zero? && 1 or f.call(n-1) } }
And somehow this should be transformed into a working
factorial
function. For the test-driven crowd, we want to write:
def test_clean_up_loose_ends
maker = ...
factorial = maker.call(
lambda { |f| lambda { |n| n.zero? && 1 or n * f.call(n-1) } }
)
assert_equal(120, factorial.call(5))
iterative_factorial = maker.call(
lambda { |f| lambda { |n, acc| n.zero? && acc or f.call(n - 1, n * acc) } }
)
tail_factorial = lambda { |n| iterative_factorial.call(n, 1) }
assert_equal(120, tail_factorial.call(5))
end
Of course, we need some code for
maker
. And the
iterative_factorial
case shows that
maker
works for functions with more than one parameter. The solution I came up with is:
CURRY = lambda { |f, a| lambda { |*b| f.call(a, *b) } }
maker = lambda { |f|
lambda { |func_with_me| CURRY.call(func_with_me, func_with_me) }.call(
CURRY.call(lambda { |inner_func, me, *args|
inner_func.call(CURRY.call(me, me)).call(*args) }, f)) }
The source code with each transformation from beginning to end is
here (I strongly suspect that this “curry combinator” is actually the Y combinator with a huge amount of cruft hanging off it).
Unique or derivative, crap or craft, the process of getting it to work has enriched my mind by forcing me outside of my usual problem space. I still can’t think of a practical application for what I’ve just written. But I know I’ve stretched myself.
And now back to you: perhaps you’re rushing off to try to implement a fixed-point combinator from first principles. Perhaps your plan is to code the canonical examples in your usual language. Those are both good paths. But whether you follow them today or not, remember the underlying principle exemplified by the fixed-point combinator:
Do not dismiss impractical or weird problems. While you may not have an immediate application for the code you write to solve such problems, you are maximizing your learning when you venture outside of your usual problem space.
- Named recursion is stuff like
foo = lambda { |...| foo.call(:bar) }
. It takes advantage of the host language’s variable binding to recurse. If you want anonymous recursion, you should be able to assign the same lambda
to another name and have it work just as well, as in: fufu = lambda { |...| foo.call(:bar) }
. That won’t work if you are relying on Ruby’s name for foo
.
p.s. Don’t miss
Tom Moertel’s derivation of the Y combinator in Ruby.
Labels: lispy, passion, popular, ruby