As you might (or might not) know, Clojure does not have support for named let expressions. Those who used (or still using) Scheme knows that named let is a cool construct to express recursion cleanly, and to be precise, the only way to do iteration in Scheme.
So, how are they used in Scheme? Here is one example you can test in Racket or Guile:
(define lst '(1 2 3 4 5)) (let loop ((lst lst)) (when (not (null? lst)) (display (car lst)) (newline) (loop (cdr lst))))
and this will print one number after another.
loop name is not part of Scheme built-in special keywords, and you can use whatever name you like (experienced Scheme developers will frequently use
lp because it is shorter to write).
In Clojure, we would express it like this:
(def lst [1 2 3 4 5]) (loop [lst lst] (when-not (empty? lst) (println (first lst)) (recur (next lst))))
Clojure has built-in loop/recur primitive(s) used for iteration and recursion. Every other construct, like for, doseq et al., is expressed through loop/recur.
As I mentioned at the beginning of this post, Clojure won't let you have something like this:
(let loop [...] ... (loop ...))
but, we can easily have it via a simple macro. Here is the code:
(defmacro letn "Named let that allows self-recursion on declared name, like in Scheme. The expression will be expanded to loop/recur, and no stack will be consumed. When the name is not given, it behaves like an ordinary 'let'." [& exprs] (if (vector? (first exprs)) `(let ~@exprs) `(clojure.template/do-template [~(first exprs)] (loop ~(second exprs) ~@(drop 2 exprs)) recur)))
As explained in the docstring, it will expand to
loop/recur expression, so it can be used for infinite recursion without the stack consumption. Here is the example from the beginning, written with the new macro:
(letn loop [lst (rage 1 10)] (when-not (empty? lst) (println (first lst)) (loop (next lst)))) ;; Or you can use a different name. Beware, loops indefinitely! (letn lp [i 0] (println i) (lp (inc i))) ;; if the name is not given, behaves like "let" from Clojure (letn [a 1, b 2] (println (+ a b)))
The nice thing about this macro is that you can use whatever name you'd like, as the Clojure expression checker will be run after macro expansion. In the end, the above expressions will be expanded to a simple loop/recur.
Technicalities & limitation
Let's discuss limitations in our initial
clojure.template/do-template will blindly replace all occurrences of the assigned name to
letn expression with
recur keyword, which is OK for most cases. However, if you have nested
letn calls, it will end up with nested
loop/recur, which is not yet supported in Clojure. To explain it easily, check the code down below:
(letn lp1 [a 1] (letn lp2 [b 2] (lp1 (inc 1)) (lp2 (inc 2)))) ;; would be expanded to something like this (loop [a 1] (loop [b 2] (recur (inc 1)) ;; where to recur, first (recur (inc 2)))) ;; or the second one?
The second issue is that our
letn doesn't allow you to call the assigned name as a function, inside the places where a function is expected, like map et al. E.g., this won't work:
(letn lp [a 1] (map lp ...)) ;; would be expanded to something like this, which doesn't make much sense (loop [a 1] (map recur ...))
This is why in Scheme, named let is usually expanded to
letrec, something like
letfn equivalent in Clojure. But, with a slight difference: in Scheme,
letrec allows proper tail calls that won't blow up the stack, but
letfn doesn't have that property.
The solution or letn v2.0
Since we can't do much about the aftermentioned limitation of Clojure compiler, we can adjust our
letn macro to use both approaches and let the developer decide which one to use. I'm not particularly eager to keep around multiple variations of
letn macro, so I'm going to use metadata as deciding factor.
Here is the improved
(defmacro letn [& exprs] "Named let that allows self-recursion on declared name, like in Scheme. The expression will be expanded to loop/recur, so no stack will be consumed. When the name is not given, it behaves as an ordinary 'let'. If ^:nested metadata was added, expressions would be expanded to use 'letfn', so 'letn' can be nested and/or called inside functions. Limitation of 'letfn' still applies - recursive calls will consume stack." (cond (vector? (first exprs)) `(let ~@exprs) ;; check if we have attached ^:nested metadata to letn name (not (:nested (meta (first exprs)))) `(clojure.template/do-template [~(first exprs)] (loop ~(second exprs) ~@(drop 2 exprs)) recur) :else `(letfn [(~(first exprs) ~(vec (take-nth 2 (second exprs))) ~@(drop 2 exprs))] ;; clojure doesn't have drop-nth, so emulate it with keep-indexed; ;; in short, keep only second elements from let expression (~(first exprs) ~@(keep-indexed #(if (= 1 (mod %1 2)) %2) (second exprs))))))
So, let's quickly overview how it behaves:
;; expands to ordinary 'let' (letn [a 1, b 2] (+ a b)) ;; expands to loop/recur (letn lp [a (range 1 10)] (when-not (empty? a) (println (first a)) (lp (next a)))) ;; expands to letfn (letn ^:nested lp1 [a '(a b c d e)] (letn ^:nested lp2 [b (range 1 10)] (cond (and (seq a) (seq b)) (do (println (first a) "|" (first b)) (lp2 (next b))) (seq a) (lp1 (next a)))))
^:nested metadata, now you control how the code is generated and how performant it can be (loop/recur usually should be the fastest option). Beware that the last example, where it displays all combinations between
(a b c d e) and
(1 ... 9) range, will loop indefinitely if
^:nested is not used. Why? I'm leaving that as an exercise to the reader ;)