Reductions Without the Boilerplate

Clojure's reduce is the natural tool for folding a collection into a single value. But once you need more than one accumulator — say, a sum and a count to compute a mean — the standard approach demands vector tuples, destructuring, and a separate let to massage the final result:

(let [[sum count] (reduce (fn [[sum count] x]
                            [(+ sum x) (inc count)])
                          [0 0]
                          [1 2 3 4 5 6 7])]
  (/ sum count))
; => 4

The awkwardness grows with complexity. The accumulator structure gets specified in several places: the destructuring bind, the function arguments, the return value, and the initial value. These pieces can drift across a screen, making it easy for the initialization to fall out of sync with the reducing function. There's also runtime cost from allocating and tearing apart those transient vectors.

Rewriting as a loop removes the destructuring and brings initial values next to their names, but introduces a new problem: traversal overhead. Each next call allocates a seq wrapper, whereas reduce walks the internal structure of a vector directly. You also end up mixing accumulation logic with explicit iteration machinery.

Loopr: The Middle Ground

(require '[dom-top.core :refer [loopr]])
(loopr [sum   0
        count 0]
       [x [1 2 3 4 5 6 7]]
       (recur (+ sum x) (inc count))
       (/ sum count))
; => 4

loopr, available in the dom-top control-flow library, combines the strengths of both forms. Like loop, it takes a binding vector of accumulator names with initial values. Like reduce, iteration stays implicit: a second binding vector declares iteration variables, and the body is evaluated for each element of the collection. The body recurs with new values for the accumulators; a final expression after the body evaluates with the accumulated bindings and provides the return value.

The syntax separates accumulators, iterator bindings, the body, and the final expression — no nesting, no destructuring of tuple accumulators, and noticeably less indentation. It also benchmarks faster than the equivalent reduce or loop above. The macro expansion chooses between iteration strategies — mutable iterators, array indexing with aget, or direct reduce — based on the collection type. For multi-accumulator reductions, extra state lives in internal volatiles, avoiding the overhead of thread-through vector tuples.

Nested Iteration, Flat Syntax

(def people [{:name "zhao"
              :pets ["miette" "biscuit"]}
             {:name "chloe"
              :pets ["arthur meowington the third" "miette"]}])

Nested collections typically force one reduce per level, threading the outer accumulator in and out of inner reductions. Indentation compounds quickly, and the innermost accumulation variable gets repeated at every level. Writing it as a loop doesn't help: you still thread accumulators through nested loop bodies, and the sequence traversal machinery gets tangled with the accumulation logic.

A single flat loop can interleave traversal and accumulation, but it's error-prone. Miss a next call, misplace a binding, and you've silently changed the semantics — easy to do even in a two-level iteration.

Clojure's for handles the nested traversal with clean syntax, but it's a map, not a reduce: there's no way to carry accumulators between iterations. loopr fills this gap by accepting multiple iteration bindings, just like for. Bindings are nested implicitly: each iteration variable has access to the values bound by the preceding ones.

(loopr [pet-names #{}]
       [person people
        pet    (:pets person)]
       (recur (conj pet-names pet)))

The result runs at the same speed as a nested reduce — the macro expands to exactly that structure — which benchmarks about 40% faster than the equivalent nested loop over seqs.

Early Exits and Array Iteration

Searching a collection can stop as soon as a value is found. Loopr supports early return just as reduce does: omit the recur. Here, iterating over a vector to find the first odd number, returning both item and index:

(loopr [i 0]
       [x [0 3 4 5]]
       (if (odd? x)
         {:index i, :number x}
         (recur (inc i))))
; => {:index 1, :number 3}

When no accumulators are declared, loopr still iterates — useful for side-effectful search. Iterator bindings support ordinary destructuring, so map entries can be bound as key-value pairs:

(loopr []
       [[k v] {:x 1, :y 2}]
       (if (= v 2)
         k
         (recur))
       :not-found)
; => :y

Without an early return, the loop falls through to the final form, :not-found.

For array-based code, the iteration hint :via :array compiles the reduction into a loop over integer indices with aget calls. Single-dimensional array reductions match (reduce + ary) in performance; multi-dimensional array reductions benchmark about 65% faster, given explicit type hints. The tactic applies per collection, so a mixed structure — a vector of vectors, say — can combine different iteration strategies in a single reduction:

(loopr [count 0
        sum   0]
       [row [[1 2 3] [4 5 6] [7 8 9]] :via :reduce
        x   row                       :via :iterator]
       (recur (inc count) (+ sum x))
       (/ sum count))
; => 5

It's a nontrivial macro, but one that is often both clearer and faster than hand-written equivalents, and one that simplifies refactoring complex reductions. Benchmarks and worked examples live in dom-top's test suite.