Transducers Compose Top-to-Bottom

Let's code some transducers and take a deeper look at how they work under the hood to see differences from pipes.


Transducers are composable reducers. A transducer takes a reducer and returns another reducer. Transducers compose via simple function composition. But, there is a tiny difference between function and transducer composition: functions compose bottom-to-top while transducers top-to-bottom.

Disclaimer: This text presumes basic knowledge of transducers and function composition.

Function Composition

First of all we discuss the basics: function composition.

Function composition is the process of chaining the output of a function to the input of another function. In algebra, composition of functions f and g means (f ⋅ g)(x) = f(g(x)).

In JavaScript we can write:

const inc = (x) => x + 1;
const double = (x) => x * 2;

const incAndDouble = (x) => double(inc(x)); // compound function

incAndDouble(2); // 6

Of course, we can do better! Let's create a general function compose:

const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);

const incAndDouble2 = compose(double, inc);

incAndDouble2(2); // 6

As you can see, the functions are applied from bottom-to-top as in the definition (f ⋅ g)(x) = f(g(x)). It means, first g(x) is applied and, then, the output is used as the input for f(). If we want a composition applying from top-to-bottom, we can do it: this kind of composition is called a pipe:

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);

const incAndDouble3 = pipe(inc, double);

incAndDouble3(2); // 6

Transducer Composition

Probably the most useful is the mapping transducer. Let's define it:

const map = f => step => (a, c) => step(a, f(c));

The map takes two curried parameters f and step. f is a mapping function and step is a reducer function to calculate (reduce) the result. We can peep at it with the following code:

const testReducer = map(double)((a, c) => console.log(c));
[1,2,3].reduce(testReducer, 0); // prints 2, 4, 6 into the console

Now we can use the composite function from above:

const incAndDouble4 = compose(map(inc), map(double));

const concat = (a, c) => a.concat([c]);

const incAndDoubleReducer = incAndDouble4(concat);

[1,2,3].reduce(incAndDoubleReducer, []); // 4, 6, 8

If you paid attention you maybe noticed one thing. Compare these two lines:

const incAndDouble2 = compose(double, inc);
const incAndDouble4 = compose(map(inc), map(double));

We didn't redefine the compose function, the result remains the same, but the order of functions for the composition is the other way around. How is this possible? Let's analyse it...

The composite function is pretty straight-forward: it takes a function from bottom-to-top, applies it (first to the init value x) and uses the output as the input for the next function. The difference between double and map(double) is, that map(double) returns a transducer function, not a scalar value as double does. This transducer function takes a parameter step, which is a reducer. It means, f(y) from the compose function is transducer(reducer); after evaluating it we get a step reducer function, which is then used as a new input y for the next transducer. Well, it means those two lines above define different kinds of function:

const incAndDouble2 = compose(double, inc);  // function (x) => x
const incAndDouble4 = compose(map(inc), map(double));  // transducer (reducer) => reducer

Composition uses reducing in the bottom-to-top direction (reduceRight), the result of the composition is the top-most transducer function which contains the next (to-bottom direction) transducer's result reducer (in the step parameter). This next reducer is applied after the current reduction (mapping) is applied.

We can write this particular composition function as:

const compose = (...reducers) => initReducer => reducers.reduceRight(
    (previousReducer, currectTransducer) => currectTransducer(previousReducer), initReducer);

Each reducer in the composition has a reference to the previous reducer (step). The final reduction starts with the top-most reducer which applies its functionality (mapping) and then executes the referenced reducer (step(...)). This brings the reducing in the opposite direction (top-to-bottom) from the transducers composition (bottom-to-top).

We can break down our example execution as following:

Transducing

As we can see, the result reducer applies functions in order: inc, then double, then concat. As expected.

Neat.

Happy transducing!