Skip to content

Commit cc0341b

Browse files
Merge pull request #15 from seancorfield/what-if-transducers
what if we taught transducers first?
2 parents 628bbc5 + fea7f8c commit cc0341b

1 file changed

Lines changed: 127 additions & 0 deletions

File tree

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
^{:kindly/hide-code true
2+
:clay {:title "What if... we were taught transducers first?"
3+
:quarto {:author :seancorfield
4+
:type :post
5+
:date "2025-05-31"
6+
:category :clojure
7+
:tags [:transducers]}}}
8+
(ns clojure.transducers.what-if)
9+
10+
;; Most Clojure tutorials start out with sequence functions like `map`,
11+
;; `filter` etc, and then explain how to avoid some of the problems that
12+
;; lazy sequences can cause. Transducers tend to be introduced later as a
13+
;; more advanced topic, but I'd argue that they could (and should) be taught
14+
;; earlier, and instead treat lazy sequences as an advanced topic.
15+
16+
;; What if... we were taught transducers first?
17+
18+
;; We're typically taught to use `map` or `filter` on a sequence or collection
19+
;; to produce a new sequence -- and there's often a comment that `map` applied
20+
;; to a vector does not produce a vector. With transducers, one of the key
21+
;; concepts is that the transformation is separated from the input and
22+
;; also from the output.
23+
24+
;; Let's start out with the `sequence` function, just to show how we can go
25+
;; straight to a sequence of results:
26+
27+
(sequence (map inc) (range 5))
28+
29+
;; `sequence` works with multiple collections, like `map`:
30+
31+
(sequence (map *) (range 5) (range 5) (range 5))
32+
(sequence (map vector) (range 5) (range 5) (range 5))
33+
34+
;; How about chaining several transformations together? We can use `eduction`:
35+
36+
(eduction (filter even?) (map inc) (range 10))
37+
38+
;; Let's look at producing different types of output, using `into`:
39+
40+
(into [] (map inc) (range 5))
41+
(into #{} (map inc) (range 5))
42+
43+
;; Under the hood, `into` uses `conj` so if you use a list, the order is
44+
;; reversed (because `conj` onto a list prepends items, whereas `conj` onto
45+
;; a vector appends items):
46+
47+
(into () (map inc) (range 5))
48+
49+
;; For the next level of control, we can use `transduce` to specify how to
50+
;; combine the results, as well as what we start with initially:
51+
52+
(transduce (map inc) conj [] (range 5))
53+
(transduce (map inc) conj #{} (range 5))
54+
(transduce (map inc) conj () (range 5))
55+
56+
;; We might be tempted to use `cons` here, but its argument order is different
57+
;; from `conj` so this will fail:
58+
59+
(try (transduce (map inc) cons () (range 5))
60+
(catch Exception e (ex-message e)))
61+
62+
;; Okay, well, let's use an anonymous function to reverse the order of the
63+
;; arguments:
64+
65+
(try (transduce (map inc) #(cons %2 %1) () (range 5))
66+
(catch Exception e (ex-message e)))
67+
68+
;; Why is it trying to call `cons` with a single argument? In addition to
69+
;; separating the transformation from the output, `transduce` also has a
70+
;; "completion" step, which is performed on the final result. A convenience
71+
;; function called `completing` can be used to wrap the function here to
72+
;; provide a "no-op" completion:
73+
74+
(transduce (map inc) (completing #(cons %2 %1)) () (range 5))
75+
76+
;; `completing` lets us provide a "completion" function (instead of the
77+
;; default which is `identity`) so we could reverse the result:
78+
79+
(transduce (map inc) (completing #(cons %2 %1) reverse) () (range 5))
80+
81+
;; Instead of producing a collection result, we can also use `transduce` to
82+
;; compute results in other ways:
83+
84+
(transduce (map inc) + 0 (range 5))
85+
(transduce (map inc) * 1 (range 5))
86+
87+
;; Now let's circle back to chaining transformations, while also controlling
88+
;; the output type. We can use `comp` for this. As a recap, here's our
89+
;; `eduction` from earlier:
90+
91+
(eduction (filter even?) (map inc) (range 10))
92+
93+
;; We can compose multiple transducers:
94+
95+
(comp (filter even?) (map inc))
96+
97+
;; Let's give this a name:
98+
99+
(def evens+1 (comp (filter even?) (map inc)))
100+
101+
(into [] evens+1 (range 10))
102+
(into #{} evens+1 (range 10))
103+
104+
;; We glossed over the result of `eduction` earlier -- it produced a sequence
105+
;; because we printed it out, but it is a "reducible" that has captured both
106+
;; its input and the series of transformations to apply, so we could pass it
107+
;; directly to `into` or `transduce` as if it were a collection:
108+
109+
(into [] (eduction (filter even?) (map inc) (range 10)))
110+
(into [] (eduction evens+1 (range 10)))
111+
112+
;; Because it is a "reducible", it only does work when it is consumed, so it
113+
;; is "lazy" in that sense, but it is not a lazy sequence. We can get a lazy
114+
;; sequence from a transducer using `sequence`, if we want, or we can rely
115+
;; on `into` and `transduce` etc being eager.
116+
117+
;; In conclusion,
118+
;; by separating the transformation from the input and the output, we gain
119+
;; expressive power, flexibility, and reuse: we can compose transducers, we
120+
;; can apply them to any input that produces values, and consume the results
121+
;; in any way we like.
122+
123+
;; For example, transducers can be used in several different ways with
124+
;; `core.async` channels:
125+
;; * [on a `chan`nel](https://clojure.github.io/core.async/clojure.core.async.html#var-chan)
126+
;; * [in a `pipeline`](https://clojure.github.io/core.async/clojure.core.async.html#var-pipeline)
127+
;; * [or consumed with `transduce`](https://clojure.github.io/core.async/clojure.core.async.html#var-transduce)

0 commit comments

Comments
 (0)