In fact, it has to do with object allocation. Here's the original algorithm, with annotations:
(defn my-lcs [^objects a1 ^objects a2]
(first
(let [n (inc (alength a1))]
(areduce a1 i
;; destructuring of the initial value
[max-len ^ints prev ^ints curr]
;; initial value - a vector of [long int[] int[]]
[0 (int-array n) (int-array n)]
;; The return value: a vector with the prev and curr swapped positions.
[(areduce a2 j max-len (unchecked-long max-len) ;;
(let [match-len
(if (.equals (aget a1 i) (aget a2 j))
(unchecked-inc (aget prev j))
0)]
(aset curr (unchecked-inc j) match-len)
(if (> match-len max-len)
match-len
max-len)))
curr prev])))) ;; <= swaps prev and curr for the next iteration
As per the Java version, prev
and curr
are "reused" - a dynamic programming approach similar to what's described here. However, doing so requires allocating a new vector on every iteration, which is passed to the next reduction.
By placing prev
and curr
outside the areduce
and making them ^:unsynchronized-mutable
members of the enclosing IFn
object, he avoids allocating a persitent vector on each iteration, and instead just pays the cost of boxing a long
(possibly not even that).
So the "ugly" trick was not in a prior iteration of his Clojure code, but rather the Java version.