I have been testing concat behaviour.
The docstring says:
Returns a lazy seq representing the concatenation of the elements in the supplied colls.
However, it seems that concat does not behave lazily for its arguments. Instead we observe the usual eager evaluation. This is not what I would expect.
Observe:
Here is simple code to generate a binary tree holding integers from "The Joy of Clojure, 2nd edition", p. 208:
; we have a binary tree based on records, holding a val and having left
; and right subtrees
(defrecord TreeNode [val left right])
; xconj basically is insertion sort; inserts value v into tree t.
; + The code in JoC is more compact; here, "explicited" for readability.
(defn xconj [t v]
(cond
(nil? t) (TreeNode. v nil nil)
(< v (get t :val)) (TreeNode. (get t :val)
(xconj (get t :left) v)
(get t :right))
:else (TreeNode. (get t :val)
(get t :left)
(xconj (get t :right) v))))
; Convert a tree into a seqs (in-order traversal, so the seq will spit
; out the integers in order sorted ascending).
; Returns a lazy seq as "concat" returns clojure.lang.LazySeq
; + The code in JoC is more compact; here, "explicited" for readability.
(defn xseq [t]
(when (some? t)
(concat (xseq (get t :left))
[ (get t :val) ]
(xseq (get t :right)))))
; "xseq" is a bit mute; add some printout to probe behaviour (watching
; out to not destroy laziness when doing so)
(defn xseq-p1 [t k]
(if (nil? t) (println k "▼" "⊥") (println k "▼" (get t :val)))
(when (some? t)
(concat (xseq-p1 (get t :left) (str k "[" (get t :val) "]" "◀"))
[ (get t :val) ]
(xseq-p1 (get t :right) (str k "[" (get t :val) "]" "▶")))))
; create a tree for testing
(def ll (reduce xconj nil [3 5 2 4 6]))
Now, querying the type of the value returned by xseq-p1 shows that it traverses the whole tree?!
([3]◀[2]▶ ▼ ⊥ means found 3, went left, found 2, went right, now at nil)
(type (xseq-p1 ll ""))
; ▼ 3
; [3]◀ ▼ 2
; [3]◀[2]◀ ▼ ⊥
; [3]◀[2]▶ ▼ ⊥
; [3]▶ ▼ 5
; [3]▶[5]◀ ▼ 4
; [3]▶[5]◀[4]◀ ▼ ⊥
; [3]▶[5]◀[4]▶ ▼ ⊥
; [3]▶[5]▶ ▼ 6
; [3]▶[5]▶[6]◀ ▼ ⊥
; [3]▶[5]▶[6]▶ ▼ ⊥
; clojure.lang.LazySeq
Making xseq actually lazy demands an additional lazy-seq in front of concat:
(defn xseq-p2 [t k]
(if (nil? t) (println k "▼" "⊥") (println k "▼" (get t :val)))
(when (some? t)
(lazy-seq
(concat (xseq-p2 (get t :left) (str k "[" (get t :val) "]" "◀"))
[ (get t :val) ]
(xseq-p2 (get t :right) (str k "[" (get t :val) "]" "▶"))))))
Now it is lazy:
(type (xseq-p2 ll ""))
; ▼ 3
; clojure.lang.LazySeq
(take 2 (xseq-p2 ll ""))
; ▼ 3
; ([3]◀ ▼ 2
; [3]▶ ▼ 5
; [3]◀[2]◀ ▼ ⊥
; [3]◀[2]▶ ▼ ⊥
; 2 3)
Is this expected?
P.S.
An alternative is to lazify either both descents (or just the rightwards one). With both descents lazified, xseq-p3 is even lazier than xseq-p1:
(defn xseq-p3 [t k]
(if (nil? t) (println k "▼" "⊥") (println k "▼" (get t :val)))
(when (some? t)
(let [ left (get t :left)
v (get t :val)
right (get t :right)
l-seq (lazy-seq (xseq-p3 left (str k "[" v "]" "◀")))
r-seq (lazy-seq (xseq-p3 right (str k "[" v "]" "▶"))) ]
(concat l-seq [v] r-seq))))
(type (xseq-p3 ll ""))
; ▼ 3
; clojure.lang.LazySeq
(take 2 (xseq-p3 ll ""))
; ▼ 3
; ([3]◀ ▼ 2
; [3]◀[2]◀ ▼ ⊥
; [3]◀[2]▶ ▼ ⊥
; 2 3)
lazy-catmight help here. - gltslazy-catinstead ofcatinxseq-p1makes it behave likexseq-p3! That's a win. But then what is "lazy" aboutconcat? I would say, it only behaves lazily if its arguments were lazy in the first place. - David Tonhofer