clojurefixpoint-combinators

fix-point combinators in clojure


One of my favorite ways to test the power of a language I'm learning is to try and implement various fixed-point combinators. Since I'm learning Clojure (though I'm not new to lisps), I did the same for it.

First, a little "testable" code, factorial:

(def !'
  "un-fixed factorial function"
  (fn [f]
    (fn [n]
      (if (zero? n)
        1
        (* n (f (dec n)))))))

(defn !
  "factorial"
  [n]
  (if (zero? n)
    1
    (apply * (map inc (range n)))))

For any combinator c I implement, I want to verify that ((c !') n) is equal to (! n).

We start with the traditional Y:

(defn Y
  "pure lazy Y combinator => stack overflow"
  [f]
  (let [A (fn [x] (f (x x)))]
   (A A)))

But of course Clojure is not nearly so lazy as that, so we pivot to Z:

(defn Z
  "strict fixed-point combinator"
  [f]
  (let [A (fn [x] (f (fn [v] ((x x) v))))]
   (A A)))

And indeed, it holds that (= ((Z !') n) (! n)).

Now comes my issue: I cannot get either of U or the Turing combinator (theta-v) to work correctly. I suspect with U it's a language limit, while with theta-v I'm more inclined to believe it's a misread of Wikipedia's notation:

(defn U
  "the U combinator => broken???"
  [f]
  (f f))

(defn theta-v
  "Turing fixed-point combinator by-value"
  [f]
  (let [A (fn [x] (fn [y] (y (fn [z] ((x x) y) z))))]
    (A A)))

A sample REPL experience:

((U !') 5)
;=> Execution error (ClassCastException) at fix/!'$fn (fix.clj:55).
;=> fix$_BANG__SINGLEQUOTE_$fn__180 cannot be cast to java.lang.Number
((theta-v !') 5)
;=> Execution error (ClassCastException) at fix/theta-v$A$fn (fix.clj:36).
;=> java.lang.Long cannot be cast to clojure.lang.IFn

Can anyone explain

  1. Why these implementations of U and theta-v are not working; and
  2. How to fix them?

Solution

  • Your definition of theta-v is wrong for two reasons. The first is pretty obvious: you accept f as a parameter and then ignore it. A more faithful translation would be to use def style, as you have for your other functions:

    (def theta-v
      "Turing fixed-point combinator by-value"
      (let [A (fn [x] (fn [y] (y (fn [z] ((x x) y) z))))]
        (A A)))
    

    The second reason is a bit sneakier: you translated λz.xxyz to (fn [z] ((x x) y) z), remembering that lisps need more parentheses to denote function calls that are implicit in lambda-calculus notation. However, you missed one set: just as x x y z would have meant "evaluate x twice, then y once, then finally return z", what you wrote means "evaluate ((x x) y), then throw away that result and return z". Adding the extra set of parentheses yields a working theta-v:

    (def theta-v
      "Turing fixed-point combinator by-value"
      (let [A (fn [x] (fn [y] (y (fn [z] (((x x) y) z)))))]
        (A A)))
    

    and we can demonstrate that it works by calculating some factorials:

    user> (map (theta-v !') (range 10))
    (1 1 2 6 24 120 720 5040 40320 362880)
    

    As for U: to use the U combinator, functions being combined must change how they self-call, meaning you would need to rewrite !' as well:

    (def U [f] (f f))
    
    (def ! (U (fn [f]
                (fn [n]
                  (if (zero? n)
                    1
                    (* n ((f f) (dec n))))))))
    

    Note that I have changed (f (dec n)) to ((f f) (dec n)).