DEV Community

artydev
artydev

Posted on

I am trying to understand what are transducers - part 2

In this post I continue my exploration on transducers, the following code resume what we have seen so far :

const C = [1,2,4,5,6]

const concat = (a, b) => a.concat(b)

const mapReduce = (func, aggregator) => 
  (acc, current) =>  aggregator(acc, func(current))

const filterReduce  = (predicate, aggregator) => 
  (acc, current) => predicate(current) && aggregator(acc, current) || acc;

// applications
const mulBy5 = x => x * 5;
const isEven =  x => x % 2 == 1;
const mul7 = x => x * 7;
const add3 = x => x + 3

const oddReducer = filterReduce(isEven, concat)
const mulBy5Reducer = mapReduce(mulBy5, concat)
const add3reducer =  mapReduce(add3, concat)

Enter fullscreen mode Exit fullscreen mode

So, we have expressed map and filter in terms of reduce

That allows to us to write the following code :


let transformedC = C
  .reduce(mulBy5Reducer, [])
  .reduce(add3reducer, [])

Enter fullscreen mode Exit fullscreen mode

Here we still create intermediate collections, nothing has changed to resolve the problem.

At this point it's time do read the definition of function composition :

In mathematics, function composition is an operation ∘ that takes two functions f and g, and produces a function h = g ∘ f such that h(x) = g(f(x))

Sadly, reducers (ie : mulBy5Reducer, add3reducer) are not composable.

The way I'm trying to solve the problem is purely experimental, and didn't lead to the 'canonical' solution, this will be for a next post.

Indeed, in the previous post I tried to use the 'transducer' notion, but I still don't get it in it's entirety

But anyway, let's try another path.

In our code we actually have 'composable' functions, those are the functions (applications) used in our reducers, ie : mulBy5, isEven...

So here are the steps I followed :

Starting from one reducer, for exemple :

const mulBy5Reducer = mapReduce(mulBy5, concat);

Enter fullscreen mode Exit fullscreen mode

The question is the following : how can we 'extract' the application mulby5 in order to compose it ?

And that leads to another question, how can we make *mulBy5Reducer * the result of a function call.

Let's call this function T for now.

So what we want is :

let chainedTransformation = C
  .reduce(T(), [])

Enter fullscreen mode Exit fullscreen mode

with :


T = () => mulBy5Reducer

Enter fullscreen mode Exit fullscreen mode

Let's continue our transformations:


T = () => mapReduce(mulBy5, concat);
T = mapReduce => (mulBy5, concat);
T = mapReduce => mulBy5 => concat;

Enter fullscreen mode Exit fullscreen mode

Note that I have isolated mulBy5.

We can then write our transformation like this :


let chainedTransformation = C
  .reduce(T(mapReduce)(mulBy5)(concat),[])
  .reduce(T(mapReduce)(add3)(concat),[])
Enter fullscreen mode Exit fullscreen mode

It's now time to see how we can compose our applications;


  C.reduce (
    T(mapReduce)
      (compose 
        (
          mulBy5, 
          add3
        )
      )
  (concat), [])


Enter fullscreen mode Exit fullscreen mode

Here the final code : Demo


// Given a collection 'C'
const C = [7,5,3];

const concat = (a, b) => a.concat(b);
let compose = (...fns) => (initialVal) => fns.reduceRight((val, fn) => fn(val), initialVal);

const mapReduce = (func , aggregator) =>  (acc, current) =>  aggregator(acc, func(current))
const filterReduce  = (predicate, aggregator) => (acc, current) => predicate(current) && aggregator(acc,current) || acc;

const add5 = x => x + 5;
const isEven =  x => x % 2 == 1;
const mul7 = x => x * 7;

let T = (transformation) => reducer => init => transformation(reducer, init);  

const decoratePredicate =  predicate => x => predicate(x) && x || [] 

const add2 = (x) => x + 2;

const appcomposed = compose (
        decoratePredicate(isEven),
        add5, 
        add5, 
        add2
      )


const transformation = C.reduce(T(mapReduce)(appcomposed)(concat), [])

console.log(transformation)

Enter fullscreen mode Exit fullscreen mode

I had to decorate the filter isEven in order to not return a boolean.

Note that instead of compose you can use pipe operator.
It's perhaps easier to read : DemoWithPipe:

// Given a collection 'C'
const C = [7,5,3];

const concat = (a, b) => a.concat(b);
let compose = (...fns) => (initialVal) => fns.reduceRight((val, fn) => fn(val), initialVal);
let pipe = (...fns) => (initialVal) => fns.reduce((val, fn) => fn(val), initialVal);

const mapReduce = (func , aggregator) =>  (acc, current) =>  aggregator(acc, func(current))
const filterReduce  = (predicate, aggregator) => (acc, current) => predicate(current) && aggregator(acc,current) || acc;

const add5 = x => x + 5;
const isEven =  x => x % 2 == 1;
const mul7 = x => x * 7;

let T = (transformation) => reducer => init => transformation(reducer, init);  

const decoratePredicate =  predicate => x => predicate(x) && x || [] 

const add2 = (x) => x + 2;

const app_piped = pipe (
       (x) => x * 2, 
       (x) => x + 9,
       (x) => x * 2 - 5,
       (x) => x - 30 
);

const transformation = C.reduce(T(mapReduce)(app_piped)(concat), []);

console.log(transformation);
Enter fullscreen mode Exit fullscreen mode

At this point I still don't know with certainty what to call a transducer in my code.

But, I am happy anyway, I have reached an important part of my goal, which was to transform a collection while performing a single iteration on it.

I hope this article have gave you the curiosity to learn deeper
on reducers an transducers.

Of course, in real life I would use a well established library like RamdaJS.

Don't hesitate to comment and correct me.

Top comments (0)