Rich Hickey – Inside Transducers [video]
youtube.com
youtube.com
That that for what you will, but please be aware that a "gift for simplification" sometimes is a double-edged sword and can leave the audience thinking that they've understood when, actually, they haven't.
I'm not saying that's the case here, just something to be wary of.
The name "transducers" is not an invention, it's basic computer science. The idea of using transducers to abstract over an input is not really controversial in the sense that it's the essence of what turns source into executable code on digital computers. Sure, Ther are tradeoffs relative to which flavor of Turing machine underpins any abstraction, but at least this is an area where we can achieve clarity.
This is something Hickey implemented, not invented. Like all of automata, understanding the concept is likely non-trivial for some pretty smart people since it requires thinking about first principles of computing.
"The reduce function is the base transformation; any other transformation can be expressed in terms of it (map, filter, etc)."
This seems so obvious in retrospect -- I can't believe I had never made that connection before.
For JS, I personally prefer lo-dash for this kind of work, or dropping in polyfills from MDN.
For some context, here are .map functions applied to normal arrays http://jsperf.com/native-vs-array-js-vs-underscore/54
I've been looking for similar libraries that work on typed-arrays because they are so much more efficient when working with web-workers or with raw canvas data. My attempts at hacking it in feel like they are just bad ideas: http://jsperf.com/float32array-map/2
When you reduce with function f(T1, T2) -> T3 the result of previous iteration becomes first argument for the next iteration, so they must be of the same "type".
Or am I missing something?
I was thinking that mapping can be f(T1)->T2 and how reducing can change types too, but I guess I wasn't paying enough attention because of course the reduce signature is f(accumulator, input) and the output is accumulator, so yea, you're absolutely right: f(T1,T2)->T1
-- Mogensen-Scott encoding
data ScottList a = ScottList (forall r. (a -> ScottList a -> r) -> r -> r)
-- Boehm-Berarducci encoding
data ChurchList a = ChurchList (forall r. (a -> r -> r) -> r -> r)
Roughly, the first encoding gives you pattern matching, and the second gives you foldr (reduce). Either of these operations is sufficient to do anything with the list.Also note that both of these are encodings of lazy (potentially infinite) lists. To encode strict (guaranteed finite) lists, you really need algebraic data types like in ML, the visitor pattern can't do that.
[0] Graham Hutton, "A tutorial on the universality and expressiveness of fold", J. Functional Programming 9(4): 355–372, July 1999. http://www.cs.nott.ac.uk/~gmh/fold.pdf
(Shameless plug in the hope it may be helpful to someone)
[1]: http://simplectic.com/blog/2014/transducers-explained-1/
- it was referenced in this great post detailing the transducer notion with Clojure, Scala, and Haskell: http://blog.podsnap.com/ducers2.html
Please could someone tell me if this is conceptually different from ruby's Enumerable module which only needs the class its included in to implement `each` so anything can be enumerable ? Or is it a similar but just in translated to the FP world ?
1. Transducers are parallel under the covers. Since the expectations is that the code that the predicate or mapping functions that you pass into map, filter and reduce are pure (no variables are changed, no state is modified, just a calculation that is only dependent on arguments) the parallelism is hidden away from you, but it's there. Ruby's Enumerable can't do parallelism
2. When you compose transducers, there are no intermediate sequences generated. The simplest example is this:
In Ruby: [1, 2, 3].map {|x| x + 1}.map {|x| x+ 2} will generate an intermediate array [2, 3, 4] after the first map, and then will generate [4, 5, 6] when it has evaluated the whole expression.
In Clojure (I am sure I got the syntax wrong for this one) (transduce (map #(+ 2 %) (map inc)) [1 2 3]) will create an intermediate mapping function that will first increment, then add 2 to its argument, and then will map once using that intermediate function.
The big thing is no intermediate results.
Functions that work over seqs/enumerables return a new seq/enumerable. From what I understand, and I'm sure I am wrong, a transducer receives and transforms a value, and may call the next transducer with the transformed value.
The nice thing is that a transducer does not create intermediate results (a seq/enumerable), and that it doesn't make any assumptions on the underlying datastructure. So you can, for instance, send a value through a transducer and directly place it in a list, instead of creating a set of enumerables and then call .ToList on that. Since transducers can work with any value, you also aren't tied to enumerables. So transducers should be a little more universal and performant than enumerables.
Enumerable.Range(1, 100)
.Where(n => n % 2 == 0)
.Select(n => n * 2)
.ToList();
And as a transducer it could looks something like this? sequence(
Enumerable.Range(1, 100),
compose(
filter(n => n %2 == 0),
map(x => x + 1)
)
).ToList();
On the linq side it would create only 2 enumerable objects (one for where and select) and the ToList would result in a copy the object pointer as it was passing through each enumerable. For the transducer example rather than having 2 enumerable's we would have 2 transducer objects?I absolutely understand the case where a language's map/filter/reduce/etc results in a whole new list, but for the above case the performance/memory overhead improvement doesn't see that huge and really minor. The fact that it removes the overhead of having to support IEnumerable and as you mentioned is only dealing with functions is what seems like a win. At first I was thinking that plain old LINQ is also more readable, but my contrived c# transducer example doesn't look that bad in the end. Am I incorrect on any of this?
With transducers I can describe a transformation that should happen to /some/ reducable input, and in the end have something that represents just the transformation. I. An then apply that to any reducable input I have.
[edit]
Your example becomes:
var incrementedEvens = compose(
filter(n => n %2 == 0),
map(x => x + 1)
);
transduce(Enumerable.range(0, 100), incrementEvens);
var lowerAlph = compose(
filter(x => Character.isAlpha(x)),
map(x => x.ToLowercase())
);
System.in.setTransducer(lowerAlpha);
You can really do that in Linq.(1..Float::INFINITY).lazy.map{|a| p a; a*10}.map{|a| p a+1} # => 1 11 2 21 etc...
So it can work with streams & so on... But I know (from a Jessica Kerr presentation) it also has limitations there.
Clojure transducers are isomorphic to functions of type `a -> [b]`, that is, functions which take some thing and return lists of other things. You can implement map and filter in this language:
tmap f a = [f a]
tfilter p a = if p a then [a] else []
You can also compose two such functions: concat :: [[x]] -> [x]
concat [] = []
concat (x : xs) = x ++ concat xs
concatMap f list = concat (map f list)
compose lb_a lc_b = \a -> concatMap lc_b (lb_a a)
One reduction function (`foldr`) turns out to just be the identity function [a] -> [a], as I recall.The interesting thing about Clojure transformers is that they have a strange continuation-passing form, such that this function `compose` that I wrote above for the type `a -> [b]` is actually in Clojure just a function composition. That is, instead of `a -> [b]` we see `forall r. (b -> r -> r) -> (a -> r -> r)`, which composes with normal function composition.
Transducers overall are quite intressting, I am exited to see what other transducer context people come up with.
There's also definitional issues e.g. can you have an async transducer?
Which might be okay, but my basic point that it's definitionally fuzzy still holds.
Transducers are defined such that you can abstract the context of input, output and iteration and focus on the transformation of each element from an independent source individually. The implementation of each transducer accepts another transformation in a "pipeline" (eventually the output sink) and accepts an input during an external iteration process. The implementation decides what to do with it (map changes with a function, filter ignores certain values with a predicate, etc.) You can also define transducers that send multiple values for each input (think string.split) or some that do not send any until completion (think buffered results).
Since the iteration source and sink are abstracted from the transformation, you can use the same transformation in other contexts (Promises, event streams, IO streams, CSP, etc.)
I've been experimenting with transducers in asynchronous contexts in JavaScript if anyone is interested, for example:
[1]: http://simplectic.com/projects/underscore-transducer/ [2]: http://simplectic.com/projects/underarm/ [3]: https://github.com/transduce/transduce-async [4]: https://github.com/kevinbeaty/transduce-stream [5]: https://github.com/transduce/transduce-push [6]: https://github.com/transduce/transduce-then
(I think Rich is absolutely spot on on many of the more abstract things about or industry/craft, but this is just one of those details that irks me.)
[1] http://existentialtype.wordpress.com/2011/03/19/dynamic-lang...
As always it's a trade off. As you try to capture more of the contract, you massively increase complexity, sometimes to the point of reducing usability (i.e. staring at an obtuse type error, or trying to work out how to do basic things with a function just from a Scala type)
It's my interpretation that "you can't type transducers" is an invitation to think more deeply about types and the limitations in what they can express, mixed in with some "get off my back about creating a dynamically typed language".
System.in.setTransducer(lowerAlpha);
In the LINQ/functional origami world, you could only produce new streams, not transform an existing stream in place. Because a new stream is computed, a new type can be computed. However, from what I've seen with transducers, they would be limited to basically Func<T, Predicate<T>, T> signatures.