them. 3. Explain the basics of how they are implemented. 4. Describe their tradeoffs, and how to choose between transducers and other similar mechanisms. 5. Convince you that you should probably be using tranducers today. 2
independent from the context of their input and output sources" "specify only the essence of the transformationin terms of an individual element" "are decoupled from input or output sources" "can be used in many different processes—collections, streams, channels, observables, etc." "compose directly, without awareness of input or creation of intermediate aggregates" Transducers reference page on clojure.org 3
dense—or is it densely brilliant? Every word carries unique semantic weight A single missed word can cause the reader to lose the thread Not necessarily an indictment—high density may be optimal for the experienced But having additional education resources is important 4
one form to another," from Latin transducere / traducere "lead across, transfer, carry over," from trans "across, beyond" + ducere "to lead" (from PIE root *deuk- "to lead"). — In Clojure, it may also be helpful to think of this like transform + reducer. Online Etymology Dictionary 5
2015. 20+ functions in clojure.core can produce transducers. Adopted into other languages: JavaScript (Transducers-JS, RxJS, etc.) Python Surely others Mature, deeply-integrated, and well-regarded. 6
etc.) complect transformation with sequence types / "processes" (collections, streams, etc.). e.g.map returns a lazy sequence… If we map over a vector, do we want a lazy sequence? What if we map over a set? What about "mapping" over the values passing through a channel or an observable? Should each of these require their own function(s)? (e.g. mapv) Or is there a more generic way to tackle this? 7
(more on this later) Should we be required to pay that cost if we don't ultimately want a lazy sequence? Composing transformations compounds the overhead (e.g. mapping twice) Quashing this can reduce readability n.b.mapv is only one small piece of the story 8
batches of 32 Buffered pull model Often we ignore these qualities …and sometimes get reminded when… 1. Side effects in map aren't realized 2. A lazy seq escapes a with-open and we try to read from a closed file 9
blocking control flow "Processes" connected by bounded buffers (backpressure) Max data size not bounded by memory Challenging to write directly in, say, C Differences: pipe version is actually parallel, buffered in bytes (not elements), doesn't wait for pull. (require '[clojure.java.io :as io]) (->> "transducers.org" io/reader line-seq (filter (partial re-find #"transducers")) (take 3)) cat transducers.org | grep transducers | head -3 11
We chose the resulting sequence type ([]). 3. The transformations are directly composed into a single function (that we could let, def, or pass in). 4. …but they appear to be missing arguments. 5. …and their order of application appears backwards. When starting out, I recommend focusing on the mechanical transformation from the familiar form. (->> langs (into [] (comp (filter (comp #{:lisp} :family)) (map (comp str/capitalize name :id)) (take 1)))) 15
others) have a 1-arity form which elides the input collection and returns a transducer Transducers compose with comp …in an order that may counter-intuitive—except in mechanical transformation from ->> into has a 3-arity form that applies a transducer to each element of the sequence (->> langs (into [] (comp (filter (comp #{:lisp} :family)) (map (comp str/capitalize name :id)) (take 1)))) 16
type Improved performance No intermediate sequences Efficient use of transients under the hood Compound transformations merged into one pass No lazy seq overhead Increased applicability Transform a core.async channel Extend your own abstractions with transduction (observables, Kafka streams, zippers? Anywhere you want.) À la carte laziness, eager by default 17
to avoid unnecessary work. Even worse, I could have written it like this, generating 4 intermediate sequences: This is even more likely if the pipeline is large and composed in various places. (->> langs (filter (comp #{:lisp} :family)) (map (comp str/capitalize name :id)) (take 1)) (->> langs (filter (comp #{:lisp} :family)) (map :id) (map name) (map str/capitalize) (take 1)) 20
but the ideal solution would look more like: But this requires controlling the entire transformation pipeline, and is often less readable. (->> langs (keep (fn [x] (when (#{:lisp} (:family x)) (-> x :id name str/capitalize)))) (take 1)) 21
doesn't matter n.b. lazy sequences are also a pull model, but operate in chunks (of 32) Transducers compose the individual mapping operations Meaning we can write it in the most expressive format And we don't need to control the entire transformation pipeline to get an optimal result 22
Basic numbers on a simple, multi-step mapping: Variant Time per call Alloc per call Lazy map 410.22 µs 480,296 b Eager mapv 63.66 µs 28,465 b Transducers 43.95 µs 6,264 b a great breakdown The eager version utilizing mapv is 6.5 times faster and allocates 16 times less for the same result […] The transducer version is even faster at 44 µs and even less garbage spawned because it fuses all the mappings into a single step. 23
value with these because the first value in the input collection (which would otherwise be used) doesn't have the right type to be the accumulator. (reduce (fn [acc x] (update acc x (fnil inc 0))) {} [:a :b :c :a]) {:a 2, :b 1, :c 1} 28
sometimes data (in macros) Sometimes first-order functions are data, sometimes they're behavior (higher-order functions) Sometimes maps / keywords are functions 35
function with two additional arities. Arity Purpose Description 0 Init Call the Init arity on the nested transform / transducing process 1 Completion Produce a final value or flush state 2 Step Standard reducer function behavior 39
and call whatever comes next to accumulate (zero to many times) It can't do the accumulation or the data structure gets baked in We escape infinite regress with a final non-transducer reducing function i.e. one that doesn't call out to yet another reducing function Providing this is the job of the transducible process (((map inc) conj) [] 0) [1] 43