IndexedSeq wraps arrays with an offset, for structural sharing of arrays: (def a (js/Array. 1 2 3 4)) ;; (= (rest a) (IndexedSeq. a 1) '(2 3 4))
Reduce without start value: (reduce + (IndexedSeq. a 0)) => 10 ;; correct
(reduce + (IndexedSeq. a 1)) => 6 ;; incorrect as 2 + 4, should be 9
Likewise when using a start value: (reduce + 100 (IndexedSeq. a 0)) => 110 ;; correct
(reduce + 100 (IndexedSeq. a 1)) => 107 ;; incorrect as 100 + 3 + 4, should be 109
Reduce for IndexedSeq [1] is implemented using ci-reduce [2]. ci-reduce implements reduce for any collection which satisfies ICount [sic, ICounted] and IIndexed.
The problem is that ci-reduce is passed the offset i as the index idx. Within IndexedSeq i is used as the offset. In ci-reduce idx is used as the index through the IIndexed -nth function, which uses the offset internally. This applies the offset twice, which leads to the wrong results. Replacing passing the internal offset i with the index 1 for (inc i) for the case without a start value and 0 for i at [1] for the case with a start value fixes the problem. This is also why reduce does work when i = 0.
Clarification of offset and index for IndexedSeq: (nth (IndexedSeq. (js/Array. :a :b :c) 1) 0) => :b ;; offset i = 1, index = 0
As an alternative fix, reduce can be implemented within IndexedSeq by using aget with the offset on the wrapped array, without going through ci-reduce and -nth from IIndexed, as IndexedSeq is the only user of the 4 arity ci-reduce.
Reduce broken for IndexedSeq when i > 0
IndexedSeq wraps arrays with an offset, for structural sharing of arrays:
(def a (js/Array. 1 2 3 4))
;; (= (rest a) (IndexedSeq. a 1) '(2 3 4))
Reduce without start value:
(reduce + (IndexedSeq. a 0))
=> 10 ;; correct
(reduce + (IndexedSeq. a 1))
=> 6 ;; incorrect as 2 + 4, should be 9
Likewise when using a start value:
(reduce + 100 (IndexedSeq. a 0))
=> 110 ;; correct
(reduce + 100 (IndexedSeq. a 1))
=> 107 ;; incorrect as 100 + 3 + 4, should be 109
Reduce for IndexedSeq [1] is implemented using ci-reduce [2]. ci-reduce implements reduce for any collection which satisfies ICount [sic, ICounted] and IIndexed.
The problem is that ci-reduce is passed the offset i as the index idx. Within IndexedSeq i is used as the offset. In ci-reduce idx is used as the index through the IIndexed -nth function, which uses the offset internally. This applies the offset twice, which leads to the wrong results. Replacing passing the internal offset i with the index 1 for (inc i) for the case without a start value and 0 for i at [1] for the case with a start value fixes the problem. This is also why reduce does work when i = 0.
Clarification of offset and index for IndexedSeq:
(nth (IndexedSeq. (js/Array. :a :b :c) 1) 0)
=> :b ;; offset i = 1, index = 0
As an alternative fix, reduce can be implemented within IndexedSeq by using aget with the offset on the wrapped array, without going through ci-reduce and -nth from IIndexed, as IndexedSeq is the only user of the 4 arity ci-reduce.
[1] Implementation of IReduce for IndexedSeq - https://github.com/clojure/clojurescript/blob/master/src/cljs/cljs/core.cljs#L323
[2] ci-reduce implementation - https://github.com/clojure/clojurescript/blob/master/src/cljs/cljs/core.cljs#L271