Welcome! Please see the About page for a little more info on how this works.

+1 vote
in Clojure by

The following expression prints {{1234}} and returns {{1}}:

(first (mapcat #(do (print %) [%]) '(1 2 3 4 5 6 7)))

The reason is that {{(apply concat args)}} is not maximally lazy in its arguments, and indeed will realize the first four before returning the first item. This in turn is essentially unavoidable for a variadic {{concat}}.

This could either be fixed just in {{mapcat}}, or by adding a new function (to {{clojure.core}}?) that is a non-variadic equivalent to {{concat}}, and reimplementing {{mapcat}} with it:

(defn join "Lazily concatenates a sequence-of-sequences into a flat sequence." [s] (lazy-seq (when-let [[x & xs] (seq s)] (concat x (join xs)))))

6 Answers

0 votes
by
_Comment made by: gfredericks_

I realized that {{concat}} could actually be made lazier without changing its semantics, if it had a single {{[& args]}} clause that was then implemented similarly to {{join}} above.
0 votes
by

Comment made by: eigenhombre

I lost several hours understanding this issue last month (link: 1, 2) before seeing this ticket in Jira today... +1.

(link: 1) http://eigenhombre.com/2013/07/13/updating-the-genome-decoder-resulting-consequences/

(link: 2) http://clojurian.blogspot.com/2012/11/beware-of-mapcat.html

0 votes
by

Comment made by: gfredericks

Updated {{join}} code to be actually valid.

0 votes
by

Comment made by: gshayban

The version of join in the description is not maximally lazy either, and will realize two of the underlying collections. Reason: destructuring the seq results in a call to 'nth' for 'x' and 'nthnext' for 'xs'. nthnext is not maximally lazy.

`
(defn join
"Lazily concatenates a sequence-of-sequences into a flat sequence."
[s]
(lazy-seq
(when-let [s (seq s)]

 (concat (first s) (join (rest s))))))

`

0 votes
by

Comment made by: gshayban

Though the docstring makes no lazyiness promises (except declaring its specific implementation), this seems like a defect, not an enhancement. mapcat realizes 4 underlying collections at minimum:

`
boot.user=> (defn notifying-seq [cb!] (lazy-seq (cb!) (cons :ignored (notifying-seq cb!))))

'boot.user/notifying-seq

boot.user=> (let [a (atom 0)]
(seq (mapcat (constantly [:ignored :ignored])

           (notifying-seq #(swap! a inc))))

@a)
4
`

0 votes
by
Reference: https://clojure.atlassian.net/browse/CLJ-1218 (reported by gfredericks)
...