Programming With Nothing: FizzBuzz in the lambda calculus in Ruby
experthuman.com
experthuman.com
I have to say, this is the essay I wish I had written. It's beautiful by every one of my standards of beauty, most especially in that the journey of writing it appears to be even more attractive than the pleasure of reading it.
I'm glad to read it today,
Thank you!
I highly recommend watching the video of the original presentation at http://rubymanor.org/3/videos/programming_with_nothing/ as Tom Stuart's public speaking skills made this a thoroughly enjoyable (if a little mind-bending) talk.
You don't actually need the Y combinator for any of the cases presented like mod, range, etc. Church numeral iterators are more than sufficient for the task.
I'll use Haskell to illustrate, but you could easily translate this into his subset of Ruby.
-- represent n as a Church numeral
iterate 0 f x = x
iterate n f x = f (iterate (n-1) f x)
-- m modulo n can be calculated with at most m conditional subtraction steps
mod m n = iterate m (\x -> if x < n then x else x-n) m
-- build the range back to front using a (number, list) pair as state
range m n = snd (iterate (n-m) (\(x, xs) -> (x-1, x:xs)) (n-1, []))
The mod implementation is an example of a general pattern. Whenever you can bound the number of iterations in an algorithm as a computable function of the arguments, you can implement the algorithm by computing the upper bound and iterating that many times with an iterator function that acts like the identity once it reaches its base case (for mod, the case is x < n).The range implementation displays another important method called 'tupling' or more generally 'strengthening the induction hypothesis'. It underlies the predecessor/decrement function for Church numerals which the author of the article presents but chooses not to explain; the idea is simple, if rather inspired. Rather than iteratively compute n-1 as a function of n, we will compute a more general datum, the pair (n-1, n). That might seem like a pointless change, but when formulated this way, the problem becomes surprisingly easy:
dec n = fst (iterate n (\(_, x) -> (x, x+1)) (0, 0))Does that clear up your confusion?
This could have replaced ~8 weeks of my CS languages/compilers class, and I would have understood the material better at the end of it.
I see it as a kind of entertaining academic game - a Glass Bead Game, if you will, and I intend the deep allusion - but I wouldn't put too much faith in it teaching you much about the mechanics of compilers. It's one way of decomposing semantics into more simple elements, but it's not the one chosen for almost all practical languages, which after all have to execute on silicon, not in the Lambda calculus.
It doesn't teach anything about parsers, and nor does it exercise much thinking in trees. It does emphasize recursion, but in a way that makes it seem like an absurd roundabout way of doing things, rather than something that more usually simplifies the expression of a program. Above all, it doesn't really demystify how the text of your program changes the coloured lights on your screen. It's a splendidly constructed wonderland, a pyramid of abstraction with nothing but function application at its core, but I don't think it leaves you with a lot more than a sense of having seen something very clever.
(FWIW, nothing in the presentation was new to me, so any residual sense of wonder has faded. Take that into account in my perhaps cynical judgement.)
I would argue that there's a big difference between "demystify how the text of your program changes the coloured lights on your screen" and "demystify why the text of your program changes the coloured lights on your screen". if you are interested only in the first question, you might be an engineer. if you are also interested in the second question, you might be a computer scientist...
As to your question "why", nothing about the lambda calculus will tell you anything about why your program changes the coloured lights. There is only "how" and "will", by which I mean human agency. There is no answer to "why" here, and there cannot be, because the "why" resides in people's minds. It takes no more extra effort to believe in "if" than beta reduction.
Take that single example: implementing if as a primitive rather than a function with lazily evaluated arguments means greatly increasing practicality at the cost of the sparse beauty of minimalism. 'If' is very common; optimizing it, diagnosing misuses of it, etc. is a lot harder once you've lost it in a forest of function applications.
That's not to say that the people who worked out how to do it in the first place weren't brilliant. They were, and it was a hard problem. But it's a solved one.
I agree, there is a big difference between implementation and semantics, but I would normally expect a compilers class to focus on implementation (and I wouldn't expect a typical languages or compilers class to spend half a semester teaching how lambda calculus works as a computation model).
[1]: http://diycomputerscience.com/courses/course/the-elements-of...
I especially like how he neither uses a fancy academic language which people can dismiss outright for not being practical - like Haskell. But neither do you need ridiculous amounts of boilerplate that obscures the message - like in Java. Ruby is used for real stuff and doesn't have pretentious academic baggage.
Edit: spelling.
Actually that could be a good way to filter your audience. Some of those who would be least likely to appreciate this tutorial (and would be likely to post comments complaining how pointless (no pun intended) it was) would avoid a Haskell article in the first place.
Whereas if it's in Ruby, JavaScript, or CoffeeScript, I try to read it immediately. So don't forget the class of slackers who mean to get around to Haskell fluency one day, but not today. We have a great interest in the subject matter and like Haskell in principle even if we're lazy(!) about evaluating Haskell code examples.
I think this tutorial shows what we really mean about Turing equivalence in languages, about how data structures can be represented as functions, and presents a wider point of view that opens up other possible program designs.
[1]: http://mitpress.mit.edu/sicp/
If you have time on your hands in the near future, you should definitely give SICP a go--it really is a brilliant book.
Here's a simple recursively defined number system in scheme: https://gist.github.com/1466985
Your comment highlights how simple things we take for granted as basic ideas (like if statements) may not be as axiomatic as we assume.
I did a lightning talk at SCNA this year which covered similar, if somewhat different, and certainly less comprehensive ground, which may be of interest to readers of this thread/article.
http://git.io/objects-as-closures (full code and all everything)
https://gist.github.com/1372131#file_v2.md (outline/notes)
(please don't make fun of me, Scheme peeps)
>>> ZERO = lambda f: lambda x: x
>>> FIVE = lambda f: lambda x: f(f(f(f(f(x)))))
>>> to_int = lambda f: f(lambda x: x+1)(0)
... etc.http://perl.plover.com/lambda/ aka "How to write a 163 line program to compute 1+1"
Although I really like the OP's approach since he slowly morphs a program his readers can understand, rather than constructing a programming system from scratch.
https://raw.github.com/sdiehl/church-numbers/master/church.p...
I don't have a strong CS background, so this might be a silly question... But obviously, on a computer, "code" is really data, a series of bytes that instructs the processor what to do. In a literal sense, this sort of lamba calculus implementation isn't far removed from bog standard procedural programming. What's the actual philosophical background here-- what is a function, really? What makes it special?
That said, lambda calculus is rather different from standard procedural programming. The closest thing to it in the "real world" would be functional languages such as Scheme, ML and Haskell.
What makes functions special? I think the article answered that: with very simple ingredients, namely recursive functions that take only 1 argument, you can essentially write any program that you could write with full-fledged Ruby (or any other Turing-complete programming language).
Mathematically a function is a mapping from an input domain to an output domain. In the Lambda calculus a function is essentially a tuple of a variable name and a body lambda expression. Applying the function to an argument lambda expression gives you the body expression, where all ocurrences of the variable name are replaced with the argument expression. Essentially, the function can be understood as an replacement rule.
I do not understand what you mean with "special".
But then the final piece of code would not look as awesome so whatever :)
Wasn't it Paul Graham who said that Ruby is an acceptable Lisp?
Deleted comment
Now functional programming and lambda calculus makes sense.
This is un-cking-real.
Thank you.