I've seen this bug report before: https://clojure.atlassian.net/browse/CLJ-1569.
Now, while trying to implement Clojure-like transducers in another language for educational purposes, I've realized how many things are affected by this. So some thoughts first...
Performing initialization lazily with the current implementation in general case requires extra checks in the step function and in the completion function as well in case of empty input. So it's not like there are no workarounds, but this is very unintuitive, especially with all standard transducers still having nullary versions that are never called (so in terms of implementation it's skipping a step in the process, but in terms of logic it's adding an implicit step of dropping a transformed initial value and replacing it with an unrelated value).
Before any further examples, I should note that alt-transduce in the linked issue doesn't check if the initial value is already reduced after initialization process, which I believe it should do because reduce itself doesn't do that. So my updated alt-reduce is
(defn alt-transduce
([xform f coll]
(let [rf (xform f)
result (rf)]
(rf (if (reduced? result)
(unreduced result)
(reduce rf (rf) coll)))))
([xform f init coll]
(let [rf (xform
(fn
([] init)
([result] (f result))
([result input] (f result input))))
result (rf)]
(rf (if (reduced? result)
(unreduced result)
(reduce rf (rf) coll))))))
Speaking of affected laziness and extra checks, examples are already there in the standard library. take is defined as
(defn take
...
([n]
(fn [rf]
(let [nv (volatile! n)]
(fn
([] (rf))
([result] (rf result))
([result input]
(let [n @nv
nn (vswap! nv dec)
result (if (pos? n)
(rf result input)
result)]
(if (not (pos? nn))
(ensure-reduced result)
result)))))))
...
First, take 0 still has to consume one item because it's the first time it has control. Second, even though I don't know why it's implemented in this somewhat complicated way, there's only one value to keep track of and yet two comparisons, apparently, just because it has to handle n = 0 when it receives its extra input. With a functioning initialization stage, it can be implemented like this:
(defn alt-take
([n]
(fn [rf]
(let [nv (volatile! n)]
(fn
([]
(let [result (rf)]
(if (not (pos? n))
(ensure-reduced result)
result)))
([result] (rf result))
([result input]
(let [nn (vswap! nv dec)
result (rf result input)]
(if (not (pos? nn))
(ensure-reduced result)
result))))))))
(Close to original, can be reordered if necessary, the point is that the two comparisons are in the places where they naturally should be.)
If this issue isn't fixed yet, there must be some strong reasons for that? It looks like this questioned never was answered, I guess my question is the same at this point: https://clojure.atlassian.net/browse/CLJ-1569?focusedCommentId=18296.
I’d appreciate any further insight you can offer on why this design choice has been taken.