So rather than processing the collection, passing it to the next function that processes the collection, passing it to the next... etc.. consuming all the CPU and memory that involves, you can define steps that are applied for each item in the collection thereby having the iteration through the collection happen once.
These steps (transducers) are also composable and reusable.
I suspect you know this, consider this a basic explanation for other people reading.
This is a much better and clearer explanation than the entire article.
What you've written is just the Haskell list monad or Java8 Stream.flatmap.
foldl (+) 0 (take n myList)
reduce (takeNPlus 10) 0 myList
I assume it should be (takeNPlus n)? (or (take 10 myList))This is 100% correct. It's amazing how over hyped they are.
If you have ever used C#'s LINQ you are using transducers. The fact that LINQ works item at a time instead of collection at a time is all that's being discussed. That if you say take the first two in a 1,000,000 long collection LINQ will only enumerate the first 2 items and not all 1,000,000 is the other behavior. And the way to do this is by composing operations using a "." operator into a "query" to run.
C# had had this since 2007. It doesn't require a fancy name, it doesn't require streams, it doesn't require "thought pieces" every few months for over a decade for people to "master thinking in linq". Just sequence your operations and get back to coding.
And Clojure is a really great language, but the amount of mental space occupied by transducers is a bad look for the language. Via analogy it's like watching people be amazed by for loops for over a decade. And I know they're are a lot of really smart people in the clojure community, so I honestly put it on Rich. Either on hyping or up so much when he released it like he just invented sliced bread and it's a deep advanced topic, or for how it's presented in the language that people have to understand so much beneath the abstraction layer to use it correctly.
Your average blub enterprise programmer has been using LINQ for 15 years and never needed 100 thought pieces in how to use it and reason about it. Yes it's a monad, yes it let's your short circuit, yes it's item at a time, but a user doesn't need to know lots of detail to use it. It's like watching a language community that can do calculus be continuously hypong on the fact it can do long division.
Clojure is an amazing language, transducers are not that special, figure out why they are so hyped in clojure.
Or maybe I have it backwards and linq/transducers really are partial differential equations and C# snuck it into the 4th grade curriculum and nobody noticed.
Isn't that just function composition to build the map function? I guess that where the magic can come in is that function composition is associative, which allows for some really interesting opportunities for runtime optimization that, so far as I know, haven't been seriously explored.
If IIRC technically it's a bit more. It's a monad just like function composition, but it's bind has to handle data threading and short-circuit behavior. If you squint it's not too differently than than a parser combinator over Applicative matching a sequence of characters. Composing operations to correct thread sequential data while handing short circuiting.
I'm not certain about the optimizations due to associativity. While yes function composition is associative, that just builds the query. Running the query itself must be sequential as each operation depends on the data from the prior, leaving I believe little room for optimization.
The difference with transducers is that the transformation logic is completely decoupled from the fact that this is happening in collections. That is what makes them usable outside of the context of collections, most famously in core.async where you can map/filter/mapcat/flatten/take/drop channels just like you do with collections. This only works because transducers are completely decoupled from either the fact that elements are coming from, or going into, a collection. I think it is a really fascinating and creative achievement in decoupling. Java Streams and Scala view/iterator/iterable transformations can never be reused in other contexts since they are fundamentally about the collections. Whatever kind of Observable/Async Stream/Future or other places where you might want map/filter/reduce functionality has to reimplement the whole set of these operations anew (see for example Akka Streams, RxJava)
Iterators are generic over some iterable type `U` in `E, R, T: Iterator<E, R, U|T>, U: Iterable<E>` that they either directly or indirectly capture some nested Iterator.
However that means that code composing iterators must also be generic over that iterable `U`, so you can't simply take two transformation pipelines and concatenate them, because they will both provide an element source already.
Transducers separate the transformation part and the processing part. So you can have a `E, R, T: Transform<E, R>` which doesn't have a generic type parameter for the Iterable, and a function `transduce<E, R, S: Source<E>, I: Sink<R>, T: Transform<E, R>>(source: S, tx: T) -> I` which contains all the transformation application machinery, be it iterator style `next()` operations, or stream based `pull/push` operations, and a function `comp<E, R, S>(left: Transform<E, R>, right: Transform<R, S>) -> Transform<E, S>` that composes transformations. The way transformers are implemented, this `comp` operation is actually simply function composition.
Still I think just the “next()” context is quite enough because you can have everyone use it by convention for everything, even things that aren’t collections. Like a fluent tensor library for example. This is based on a quick understanding of transducers…
```js
let pipelineA = map(i => i+1); // or makeComplexPipelineA();
let pipelineB = filter(i => i.isEven()); // makeComplexPipelineB();
let pipeline = pipelineA.combine(pipelineB);
let result = [1,2,3,4,5].transform(pipeline);
```It also reinforces my view that you should try to separate logic and control flow as much as possible. At work, we use awful callback chains. There must be a better way to express the logic and hide the callback logic, even in C++ (lots of rope to … find alternative solutions.)
Think generics in Go or concurrency (effects) in OCAML or smart pointers in Rust. Not at all unique things, but having them in the language with other benefits is worth some discussion as it may provide extra leverage in context.
(map fn1 (map fn2 (map fn3 collection)))
into
(map (fn [x] (fn1 (fn2 (fn3 x)))) collection); Is that correct?
More idiomatic clojure:
(map (comp fn1 fn2 fn3) collection)
Funnily enough, Julia is where I came across the term transducers too, via the Transducers.jl package [2]. The article and the comments here now make me wonder what the difference is, between broadcast fusion and transducers.
[1] https://julialang.org/blog/2017/01/moredots/ [2] https://github.com/JuliaFolds2/Transducers.jl/
Maybe this variant of explanation will suit your context better:
https://www.evalapply.org/posts/n-ways-to-fizzbuzz-in-clojur...
I wrote the post as a way to explore Clojure's standard library using FizzBuzz as a device.
Your comment sharpened that for me. I don't want FizzBuzz; I want someone taking a reasonable toy problem, such as a trad+photon+quadtree raytracer and demonstrating advantage by applying the concept.
This work predates the term "transducer" and I was happy to see Rich Hickey defining it, so I use the term retroactively.
Note that I don't see much use for transducers as a general programming technique. This one use is very specific, but I have never before or since needed one.
IIRC Hickey did not define this term (nor claimed to have done so), he found it in existing compsci research papers that approached them from a mathematical proof angle and realized they solved a problem he needed to solve (basically not allocating intermediate collections and doing wasteful work on parts of the data that will be immediately discarded).
I think one of the authors of the papers he cited even gave a talk at a Clojure conference once.
However, it's worth noting that transducers aren't strictly about sequence-to-sequence transformations. From its inception, the Clojure standard library was built upon the `seq` protocol, and all its collection functions were constructed using this protocol. If it were possible to implement the `seq` protocol on channels, it would address the issue. However, it's not feasible. When querying a channel about its next element, the only honest answer is, "I can't determine that without potentially waiting indefinitely until the next message comes in or the channel closes." Turning the `seq` protocol into an asynchronous IO-bound process would be problematic. This is why transducers are essential.