Split every vector in a sequence into two parts: old elements that were contained in previous vectors and new elements never before seen
Concatenating a sequence of strings

You should use (apply str) instead of (reduce str) As str can take multiple arguments, and apply can take a sequence as a second argument. Intermediate results generated by reduce would cause time and space waste.

Since you are using a separator string, use clojure.string/join instead:

(-&gt;&gt; (i :new) (map :name) (clojure.string/join ", "))


Naming loop vars

In the above code i is not a good name, I expect i to be some kind of index. If using meaningful names like user, order is not available; you can user curr for current m for map as temporary variables. Important point is principle of least surprise. 

Map destructuring

You can also use map destructuring. It is easiest to show it by example:

&gt; (doseq [{:keys [new old]} [{:new 1 :old 2} {:new 3 :old 4}]] (prn old new old))
2 1 2
4 3 4


So similarly here:

(doseq [{:keys [new old]} (rest precomputed)]
  (println "new: " (-&gt;&gt; new (map :name) (clojure.string/join ", "))
           "old: " (-&gt;&gt; old (map :name) (clojure.string/join ", "))))


Prefer functions over constants

Prefer functions over data. Functions are the basic unit of re-use in clojure. It will be obvious if you try to test precomputed you provided, with multiple example data (in REPL by hand or with run-tests). Other signs precomputed should be a function is:


it's name: precomputed implies a function by name precompute should exist (also/instead).
data is only referred to once, right at the end; and the rest of the form doesn't depend on it.


Decompose precomputed

See values in precomputed does not depend on the previous values of :new and :old but only those of :seen?.

It can be rewritten thus:

(defn seen-ids [data]
  (reductions
      #(clojure.set/union %1 (-&gt;&gt; %2 (map :id) set))
    #{}
    data))


You don't need to put parens around set.

You can then get new and old elements separately:

(defn new-elts [seen-ids data]
  (map (fn [seen? v] (remove #(-&gt; % :id seen?) v)) seen-ids data))


Terse function definition

Sometimes you may want to define a function as combination of other functions instead of using fn. Combinators clojure has for this purpose are comp, partial, and juxt.

For example #(-&gt; % :id seen?) is equivalent to (comp %1 :id), which also allows the fn to be replaced by #. You can sometimes use partial to define functions w/o using parameter names, but I do not recommend it to use in top level functions, as parameter names are important developer documentation; but this style may be useful for shortening inline definitions (in let, for etc).

old-elts can be defined using this style, like this (compare with the definition of new-elts above):

(def old-elts (partial map #(filter (comp %1 :id) %2)))