I study lambda calculus using the book "Introduction to Functional Programming through the Lambda Calculus" by Greg Michaelson.
I implement examples in Clojure using only a subset of the language. I allow only:
- Characters
- functions with one argument lambda
- attachment
- var definition for convenience.
So far I have these functions:
(def identity (fn [x] x)) (def self-application (fn [s] (ss))) (def select-first (fn [first] (fn [second] first))) (def select-second (fn [first] (fn [second] second))) (def make-pair (fn [first] (fn [second] (fn [func] ((func first) second))))) ;; def make-pair = λfirst.λsecond.λfunc.((func first) second) (def cond make-pair) (def True select-first) (def False select-second) (def zero identity) (def succ (fn [n-1] (fn [s] ((s False) n-1)))) (def one (succ zero)) (def zero? (fn [n] (n select-first))) (def pred (fn [n] (((zero? n) zero) (n select-second))))
But now I'm stuck on recursive functions. More precisely, about the implementation of add . The first attempt mentioned in the book is as follows:
(def add-1 (fn [a] (fn [b] (((cond a) ((add-1 (succ a)) (pred b))) (zero? b))))) ((add zero) zero)
The rules for calculating the lambda of reduction force in order to replace the internal call with add-1 actual definition containing the definition itself ... are infinite.
In Clojure, which is the application order language, add-1 also elvaluated before any execution of any kind, and we got a StackOverflowError .
After some mistakes, the book will suggest a fixture that is used to avoid endless replacements of the previous example.
(def add2 (fn [f] (fn [a] (fn [b] (((zero? b) a) (((ff) (succ a)) (pred b))))))) (def add (add2 add2))
The definition of add extends to
(def add (fn [a] (fn [b] (((zero? b) a) (((add2 add2) (succ a)) (pred b))))))
This is absolutely normal until we try! This is what Clojure will do (referential transparency):
((add zero) zero) ;; ~=> (((zero? zero) zero) (((add2 add2) (succ zero)) (pred zero))) ;; ~=> ((select-first zero) (((add2 add2) (succ zero)) (pred zero))) ;; ~=> ((fn [second] zero) ((add (succ zero)) (pred zero)))
The last line (fn [second] zero) has a lambda that expects one argument when applied. Here's the argument ((add (succ zero)) (pred zero)) . Clojure is an "application-oriented" language, so the argument is evaluated before the functional application, even if in this case the argument will not be used at all. Here we return to add , which will be repeated in add ... until the stack explodes. In a language like Haskell, I think that would be nice because it is lazy (normal order), but I use Clojure.
After that, the book goes long, introducing a delicious Y-combinator that avoids the pattern, but I came to the same terrible conclusion.
EDIT
As @amalloy suggests, I defined the Z combinator:
(def YC (fn [f] ((fn [x] (f (fn [z] ((xx) z)))) (fn [x] (f (fn [z] ((xx) z)))))))
I defined add2 as follows:
(def add2 (fn [f] (fn [a] (fn [b] (((zero? b) a) ((f (succ a)) (pred b)))))))
And I used it like this:
(((YC add2) zero) zero)
But I still get StackOverflow.
I tried to expand the function "manually", but after 5 rounds of beta reduction, it looks like it expands indefinitely in the parens forest.
So what trick do Clojure "normal order" and not "applicative order" without macros. Is it possible? Is this even a solution to my question?
This question is very close to this: How to implement iteration of lambda calculus using the lisp scheme? . Except that mine is about Clojure and not necessarily about Y-Combinator.