Programming for fun and profit

Programming tutorials, problems, solutions. Always with code.

Clojure transients – fast mutations in persistent world

Clojure transients is a great way to optimize performance sensitive code without leaving familiar Clojure world. In this post we show how to use them to boost performance.

Basic flow with transients

The code structure is the same as in any Clojure code, except turning mutations on using transient function, modifications using bang! versions of functions, and persisting back with persistent! function:

(defn transient-flow [v]
  (let [tv (transient v)]
    (-> tv
        ;; mutate with bang! functions

Now you can call it like any other Clojure code:

(is (= [1 2 3] (transient-flow [1 2 3])))

Transient mutation functions

Like their immutable versions Transients support read-only operations with standard functions (count, get, etc.), but for mutations they have own versions, which we will cover here.

conj! – add elements to a transient collection

(defn conj-inline [coll val]
  (let [tv (transient coll)]
    (persistent! (conj! tv val))))

The test:

(is (= [1 2 3 42] (conj-inline [1 2 3] 42)))

assoc! – create/replace mapping for a key

Here we just replace each value with its multiplication by n:

(defn times [v n]
  (loop [tv (transient v) i (dec (count v))]
    (if (<= 0 i)
      (recur (assoc! tv i (* n (get tv i))) (dec i))
      (persistent! tv))))

And test:

(is (= [2 4 6] (times [1 2 3] 2)))

dissoc! – remove mapping for a key

(defn remove-key [m key]
  (-> m transient (dissoc! key) persistent!))

Let’s run it:

(is (= {:points 1234}
       (remove-key {:points 1234 :level 4} :level)))

pop! – remove last from a vector

(defn remove-last [v]
  (-> v transient pop! persistent!))

Pop from stack seems to work:

(is (= [1 2] (remove-last [1 2 3])))

disj! – remove last from a set

(defn remove-from-set [s key]
  (-> s transient (disj! key) persistent!))

The test:

(is (= #{1 3} (remove-from-set #{1 2 3} 2)))

Transient types – vectors, maps, sets

Only persistent vectors, hash maps, and hash sets can be transient. When you try to turn an unsupported type into a transient then ClassCastException will be thrown:

(defn transient-list [l]
  (persistent! (transient l)))

The test:

(deftest should-throw-exception-for-non-transientable-types
    (transient-list (range 10))
    (throw (IllegalStateException. "Should fail!"))
    (catch ClassCastException e
      (prn "List cannot be transient: " (.getMessage e)))))

Properties of transients

There are a few things that it’s good to keep in mind when using Clojure transients:

  • creating a transient takes O(1) time, persisting it back is O(1) too
  • they require thread isolation, because share mutable state between operations.


Share with the World!