The reduction process iteratively applies provided function f to two arguments: the intermediate result of the reduction and the next unprocessed item in a given collection coll . At the beginning of the reduction, the initial value val is used in place of the intermediate result. The first code snippet above is efectivelly the same as calling (+ (+ (+ 0 1) 2) 3)

The initial value val does not have to be provided. Clojure follows the semantics of the Common Lisp, that states :

This means that the reduction process starts by applying f to the first two collection items, and the initial value is treated as a first item in the provided collection. Easy at first, this behavior has multiple edge cases that have to be treated specially, and Clojure consistently follows Common Lisp in the way how these cases are handled.

The rules are not that hard to understand and the behavior is not surprising. What this complicates is however the underlying implementation of reduce .

Simple, easy and fast. Pick two.

The initial implementation of reduce converted input coll into seq and handled all the special cases by itself. As the generic reduction of seqs is not a very efficient operation, collection types that can do better could implement IReduce interface and provide more performant implementation. It is important to note that regardless of the underlying collection type, the conversion to seq happened every time and the custom implementations of IReduce had to provide 2 separate implementations of reduce, based on whether the initial value was given or not.

In the version 1.1, The role of IReduce was diminished and chunked sequences were introduced in an attempt to provide fast reductions with less boilerplate. Collection types implementing IChunk interface only needed to provide one version of reduce, and could assume that the initial value is always given. Clojure 1.3 have increased the flexibility of custom reduce implementations by providing a level of indirection and introduced InternalReduce protocol. Once again, custom implementations only had to handle case with the initial value provided.