I am doing the The Little Schemer and The Seasoned Schemer exercises in Clojure as an exercise to learn both Scheme and Clojure - trying to write it in as idiomatic Clojure as I can. In Ch. 14 (Seasoned Schemer) they define the function leftmost, which should find the first "atom" (meaning an entity that is not a list, not the Clojure definition of an atom) in a list. This is my implementation of the true recursive version in Clojure:
(defn atom? [x] (not (coll? x))) (defn leftmost [l] (cond (empty? l) [] (atom? (first l)) (first l) :else (let [r (leftmost (first l))] (if (atom? r) r (leftmost (rest l)))))) To make it clear what it does, here are the tests for it:
(deftest test-leftmost (is (= :a (leftmost [:a :b [:c :d]]))) (is (= :a (leftmost [[:a :b] [:c :d]]))) (is (= :a (leftmost [[] [] [[:a]] :b [:c :d]]))) (is (= [] (leftmost-recur [[] [['()]]]))) (is (= [] (leftmost-recur [])))) The key part of the exercise is to "cache" the call of leftmost (first l) by using a let statement in the :else clause.
I want to write this in Clojure using recur and get tail call optimization. The best I can do so far is this:
(defn leftmost-recur [l] (cond (empty? l) [] (atom? (first l)) (first l) :else (let [r (leftmost-recur (first l))] (if (atom? r) r (recur (rest l)))))) In the :else clause I've still got a true recursion, not recur, because recur must of course appear in a tail-call position. This function passes the test, but is subject to the problems of true recursion, including stack overflows.
Is there a way to "cache" the (let [r (leftmost-recur (first l))] call without doing true recursion?
Attempt to use memoize
I tried to think about using memoize, but I'm not sure how to memoize a self-recursive function. Here was my attempt, but I don't think it is doing what I hoped:
(defn leftmost-recur-memoize [l] (Thread/sleep 100) ; added to check whether memoize is working (let [memo-leftmost (memoize leftmost-recur-memoize)] (cond (empty? l) [] (atom? (first l)) (first l) :else (let [r (memo-leftmost (first l))] (if (atom? r) r (memo-leftmost (rest l)) ))))) ... based on the test numbers:
(println (time (= :a (leftmost-recur-memoize [:a :b [:c :d]])))) (println (time (= :a (leftmost-recur-memoize [[] [] [[:a]] :b [:c :d]])))) (println (time (= [] (leftmost-recur-memoize [[] [['()]]])))) ;; repeat same (println (time (= :a (leftmost-recur-memoize [:a :b [:c :d]])))) (println (time (= :a (leftmost-recur-memoize [[] [] [[:a]] :b [:c :d]])))) (println (time (= [] (leftmost-recur-memoize [[] [['()]]])))) "Elapsed time: 100.27427 msecs" true "Elapsed time: 701.740783 msecs" true "Elapsed time: 801.796439 msecs" true "Elapsed time: 100.148838 msecs" true "Elapsed time: 701.430802 msecs" true "Elapsed time: 801.767962 msecs" true
The bottom line questions
So, at long last (sorry for the verbosity), I am asking for help on two questions:
- Is there a way to cache or memoize the statement in the original else clause without doing true/actual recursion every time?
- What would be the most idiomatic Clojure way of doing this type of thing? (which might be with higher order functions or some other approach)