The Y Combinator, or How to Succeed at Recursion Without Really Recursing
mvanier.livejournal.com
mvanier.livejournal.com
Why do hackers focus on it so much? It has only very remote relevance to normal programming.
The beautiful idea I see behind the Y combinator is that of formalism. Once you describe something in mathematical terms, magic can happen. But this would be much better illustrated by a good introduction to analysis or algebra. Even number theory has some cool results, and it's easier to grasp.
In my second course at university, the teacher proved by contradiction that 1 >= 0:
Assume 1 is negative, 1 < 0
Multiply both sides by 1, so that 1*1 > 0*1 (multiplication by a negative flips the inequality)
1 being an identity, this implies 1 > 0, contradiction
That was one of the few Wow moments I had in my education. Short and to the point.(that proof above is not really satisfactory, especially without the context and the axioms, but it served its purpose well five years ago)
2. Directly dealing with these mathematical formalisms (the Y combinator, SKI combinators (http://en.wikipedia.org/wiki/SKI_combinator_calculus), etc.) can become really important in programs such as interpreters.
2. Really? If you feel like elaborating / providing link, I'd appreciate.
As a more concrete example, I originally had words (functions, so to speak) defined Forth-style, e.g.
: avg + 2 / ;
Which roughly means, "start recording (:) for a function called 'avg' in which we pop two numbers from the stack, add them, and push the result (+), then push the number 2, pop the two numbers on the stack and divide them (so sum/2), then stop recording (;)". Then, I realized that once I added sytax to the lexer for lists ("blocks") which are pushed onto the stack as one unit, I could instead use a list of symbols like quoted code, and instead define same by [+ 2 /] "avg" def
which pretty literally means, "pop a function name off the stack, pop a block of code off the stack, and associate them in the local environment.". It no longer needs to include any state in the interpreter for whether it was in recording or executing mode, keep track of some words (e.g. ";") which must interrupt recording mode, etc. (Adding code-as-data really adds to a language's capacity for abstraction.) Similarly, "[+ 2 /] do" works for an anonymous function of sorts. Much of the rest of the language is contained by how some of the core idioms for things like evaluation and definitions work, and formalisms like the lambda calculus and combinatory logic seem to be frameworks for studying those processes explicitly.By the way, if you'd like to write a quick interpreter as a learning experience, Forth has my recommendation. It's small and (aggressively) simple, but still supports an impressive amount of abstraction. The parser for it is even simpler than for a Lisp, and that's really saying something. (Here is a nice example of one written in heavily documented i386 assembly: http://www.annexia.org/_file/jonesforth.s.txt )
Second, OCaml seems to be particularly well-suited for writing interpreters (and compilers, I'm sure), though in this case it was serious overkill.
* I'm kind of whining here, but AFAICT it isn't possible to install a working version of Factor (http://factorcode.org/) on OpenBSD (or, probably, any Unix) without downgrading to a version of the OS using the same dynamic libraries, installing their binary version, and then progressively updating both in tandem until it's running with current libraries. Maybe I just don't have enough experience bootstrapping new language environments, but that was a disappointment -- it looks like a cool language.
(Also, 1 isn't really an argument so much as an observation.)
Also see chapter 22 in Shriram Krishanamurthi's _Programming Languages: Application and Interpretation_. (http://www.cs.brown.edu/~sk/Publications/Books/ProgLangs/200...)
The goal of the chapter is essentially, "Now that we've built our own small programming language on top of Scheme, how can we reduce the number of Scheme elements that it relies on directly?" Among other things, he shows how to use the Y combinator to allow recursive function definitions without having to use Scheme's define.
Understanding a new concept feels good intellectually, but it also gives you more power: now you can recognize that pattern elsewhere and perhaps apply new techniques.
Quicksort would be pretty good for this, but the straightforward recursive version has several edge cases that lead to bad performance.
Getting the length of a linked list is probably a better example:
(* ocaml *)
let rec length l = match l with
| [] -> 0
| h::t -> 1 + length t
; scheme
(define (length l)
(if (null? l)
0
(+ 1 (len (cdr l))))
Neither of these is tail-recursive, of course, but that's another post entirely.Any other ideas?
I vote for quicksort - not because it's quick, but because its implementation has the same form as its proof. It's simple if you use the Erlang-style version.
Map might be another good one, although for better and worse it expects the reader to follow the idea of first class functions. Something like
map square [1, 2, 3, 4, 5] ---> [1, 4, 9, 16, 25]
is pretty simple, though.I can only think of a couple other suggestions for simple recursive functions. Euclid's algorithm for GCD would be a good one. Fibonacci numbers are easily expressed with recursion, but the most obvious solution is unfortunately not tail recursive and inefficient. My favorite example of mutual recursion is "even" and "odd":
even 0 = True even x = odd (x - 1)
odd 0 = False odd x = even (x - 1)
Inefficient, of course, but easy to prove correctness even to someone who's brain hasn't yet clicked into understanding recursion. Length of a list is as boring as factorials, but I guess that would be one too. Same with generating a list of counting numbers.
Euclid's algorithm is a really good example, by the way. Thanks.