Transducers

In notebook:
Work Notes
Created at:
2019-04-02
Updated:
2019-04-03
Tags:

Trying to understand transducers, this is what I got so far:

The main benefit of using transducers is that you can batch multiple operations on items of an iterable (lists, but it can be observables, or Objects). These operations can change the values of individual items (think x => x+1) or filter them (x => x > 2). For example in Ramda many functions can be used as transducers, for example R.take(n) which filter the items based on their position in the list.

The transducer function is actually a very complex function and in most cases you would use ones provided by libraries.

Almost all articles and tutorials start by creating a transducer function. In the end, you learn the internals of the transducer function, and it's important, so that you don't confuse them with "regular List function". They start from a List.map and List.filter functions and start to "recreate" them, so that 1. they can be composed together (batch operation see above) 2. all internal operations are passed in as functions and they no longer depend on either on .map or .filter or even .reduce.

The example form Kyle Simpsons book:

function mapReducer(mapperFn,combinerFn) {
    return function reducer(list,val){
        return combinerFn( list, mapperFn( val ) );
    };
}

Note, that this format needs to be curried so that you can compose several transducer function together: they pass the combiner function (accumulator/iterator) to the next transducer.

Or, from the article Understanding Transducers in JavaScript:

const mapping = f => reducing => (result, input) =>
  reducing(result, f(input));
const filtering = predicate => reducing => (result, input) =>
  predicate(input) ? reducing(result, input) : result;

My observations on these:

  1. Transducer functions are much more complex that something that I would want to author directly (see the above examples). I think it's better to combine existing transducers provided by libraries. Ramda docs mention if a certain function can be used as transducer (see this quesiton on GitHub). Usually library implementations expect your transducers to have certain methods (.step etc.), see article on transducers

  2. They work on individual items of a List (in the broad term), even though they have names like map or filter or that R.map or R.filter or R.take can be used as transducers. Normally you got used to that these functions work on lists and return lists, but in the case of transducers they work on individual items! There's no reference to Array or Object or whatever in the implementation of these transducer functions.

      1. The accumulator or iterator function is passed into your tansducer function (combinerFn or reducing). This is why writing your own reducers may be too complex, this is why transducers work on individual items and this is why thy can be composed together.

about compose: while R.compose is normally right to left, when used to compose transducers, it's executed left to right!

Libraries like Ramda provide a way to run your transducers:

R.transduce(transducerFn, accumulatorFn, acc, List)

transducerFn: your transducer function, usually a composition of several transducers (eg. you filter and map the values). This (the composition) is the point of using transducers!

accumulatorFn: this will be passed into your transducerFn and each transducer in the composition will pass it to the next one. Then on each item iteration, after you filtered and transforme an individual value it runs with the acc value and the result of your transducers composition. Think Array.reduce.

List: the list you want to iterate over. The point of using transducers (see intro part of this article) is that the filtering and mapping work on individual items (yes, I'm repeating myself) and so this can used on many different kinds of iterables, e.g. event streams (RxJS) and observables. This is the second reason why they are so useful, they are much more efficient when dealing with Lists and complex transformations.

Finally, note the existence of acc and List parameters. The input is a List and the output has the shape of acc.