Transients in clojurescript

Clojure’s transients and their conj!, assoc! functions also exist in Clojurescript. A requirement for using transients in Clojure is thread isolation Clojure - Transient Data Structures.

But JS is not multithreaded - so, my question is: is there a performance benefit from using transients in Clojurescript, and if so, how is that achieved if it is not assuming thread isolation?

1 Like

Yes, transients gain a similar performance benefit in CLJS as in CLJ.

The isolation part really only means that transients aren’t safe to use anywhere outside the code that is supposed to construct them unless turned persistent! first. This is just as true in CLJS as it is in CLJ. It doesn’t actually require threads. Transients mutate in place instead of creating new instances for every operation. This is where they get their performance benefits from, they don’t actually do anything with threads themselves to gain that performance.

Suppose you try

(def x (transient {}))
(assoc! x :x 1)
(persistent! x)

One might assume to get an empty map as a result of the (persistent! x) since x started as an empty map and the return value of the assoc! was ignored. But you do actually get {:x 1} due to the aforementioned “mutate in place”. Also (assoc x :y 1) after persistent! has been called yields an error.

This is also a mistake people make and maybe unknowingly abusing that fact. assoc! must be used just as assoc. So, the return value must be used and previous versions discarded. Otherwise weird things will start happening. Threads just add to the ways you can possibly use transients wrong.

1 Like

Thanks @thheller! Are there any constraints on transients in Clojurescript? Naïvely put, could I use transients in any place that I use their non-transient counterparts?

I think I realized the answer to my second question - I lose immutability, right?

But let’s say that I am reducing over a collection to produce a new collection, like this

(persistent!
  (reduce 
    (fn [acc v] (assoc! acc (:id v) v))
    (transient {}) 
    some-coll-of-maps)

Would this always lead to a performance benefit if the collection includes, say, over 1000 maps? I guess there is some overhead when calling persistent!?

Again, answering my own question. I just checked the source for group-by, which is similar to my example function and it actually uses transients group-by. So I guess that it is a good idea to use transients in reducers and other places where only the end result needs to be immutable.,

Not quite all such places! Transients do not have all of the retrieval capabilities of persistent collections. Clojure 1.12 is to narrow the gap: Clojure - Clojure 1.12.0-alpha1