I think that a good way to understand transducers is to look at their implementation (shortened a bit). Here it is for map:
([f]
(fn [f1]
(fn
([result input]
(f1 result (f input)))
([result input & inputs]
(f1 result (apply f input inputs))))))
filter:
([pred]
(fn [f1]
(fn
([result input]
(if (pred input)
(f1 result input)
result)))))
And it gets more interesting with take:
([n]
(fn [f1]
(let [na (atom n)]
(fn
([result input]
(let [n @na
nn (swap! na dec)
result (if (pos? n)
(f1 result input)
result)]
(if (not (pos? nn))
(reduced result) ; a terminal value indicating "don't reduce further"
result)))))))
The transducer is supplied with the reducer next in the chain (f1) and returns a reducer function that gets fed with the reduced value by the preceding reduction (result) and the next element (input). Note how the take transducer maintains internal state with an atom. This could get a little tricky for more elaborate reductions, as
how the internal state is maintained might have a significant effect on performance, depending on exactly how the reduction is performed. For example, if the reduction is done in parallel (say, with fork-join), then an internal state that's updated with locks (like refs) might significantly slow down -- or even deadlock -- the reduction.
AFAICT mapcat still only returns lazy-seqs.
AFAICT mapcat still only returns lazy-seqs.