I've been actively using Clojure for more than 5 years now. It's no surprise I grew fond of its data transformation capabilities.
All these little functions, operating over a handful of data structures. All these beautiful transformation pipelines. You know, the ones with arrow after opening brace (-> or ->>). The threading macros.
It's probably my favorite syntactic sugar. The device to turn lispy, nested kind of code to more humane, easier on the eyes.
So, instead of
(reduce + (map :amount (filter cash? payments)))
I can write
(->> payments
(filter cash?)
(map :amount)
(reduce +))
Sure, it's more lines, but it's easier to read because you don't need to follow right to left, innermost to outermost flow of data. Nothing stops me to write it as one liner too, I just prefer not to.
But then, transducers. They were introduced in Clojure 1.7 and seemed great. Less garbage, more performance, same old map and filter. But old habits die hard and I never really tried to use them.
Recently, something changed.
I've profiled some code and started to see unnecessary inefficiencies in every little transformation with all those intermediate sequences, born to die the next moment.
I've read A History of Clojure where Rich mentions he should've introduced transducers earlier. Like make them the primitive everything else is built on top of.
So, I tried. And they felt a little awkward. My favorite trick of using ->> was gone.
Take a simple pipeline you saw before:
(->> payments
(filter cash?)
(map :amount)
(reduce +))
You start with value and see how it flows through a number of steps. From top to bottom, producing result. It's natural to read, it's step by step, it's sequential.
Now look at the same thing but with transducers:
(transduce
(comp (filter cash?)
(map :amount))
+ payments)
Suddenly, it becomes nested again. Harder to read. The same pipeline is still there but shoved into blaring comp. Initial value no more in the start and reduction (transduce) and its reducing function are separated from each other.
Sure, I can make it slightly better by lifting the pipeline out of the form.
(let [cash-amount (comp (filter cash?)
(map :amount))]
(transduce cash-amount + payments))
But now when reading top to bottom you encounter transformation first, without the knowledge of the value it is intended to transform. You need to read on to the reduction itself to learn the value and then go back and reread transformation to understand the result.
It's harder to read and awkward to write, but it seems like I'm expected to use transducers this way. Separating the transform and naming it. Even official documentation does this.
But I don't want to name things unless I must. Especially for ad-hoc transforms that you can see in Clojure code all the time.
What if I could bring back some of the nice top to bottom feel to it? Have all the benefits and ergonomics of threading macro.
Something like this:
(t-> payments
(filter cash?)
(map :amount)
(transduce +))
Since ->> is only a macro what stops me from making a macro of my own (besides the little indecision everyone feels)?
So, here it is:
(defmacro t-> [v & forms]
(let [[op & args] (last forms)
xf (if (#{'transduce 'into} op)
(butlast forms)
forms)]
(case op
into `(~op ~@args (comp ~@xf) ~v)
transduce `(~op (comp ~@xf) ~@args ~v)
`(sequence (comp ~@xf) ~v))))
Now I can thread. But would I?