loading...
Cover image for RamdaJS: transduce

RamdaJS: transduce

rodw1995 profile image Robbie op de Weegh ・4 min read

When I was looking at the Ramda docs I came across the transduce function. And at first I found it rather difficult to understand what exactly the function did and what you could do with it. So I did some reading about transducers in general and started trying some things to fully understand. In this post I will describe the steps I took towards fully understanding this transduce function.

What is a transducer?

First things first. What are transducers and what can we do with them? Well, you may have guessed it already, but the word transduce is just a combination of the words transform and reduce. And that is also what it does:

A transducer transforms items while reducing them.

I assume you have an understanding of what reducers are. If not, there are a lot of resources out there that will help you and it's not that difficult.

Doc example

Now we have some idea of what a transducer does, we can have a look at the example code from the Ramde docs and try to understand:

const numbers = [1, 2, 3, 4];
const transducer = R.compose(R.map(R.add(1)), R.take(2));

R.transduce(transducer, R.flip(R.append), [], numbers); //=> [2, 3]

So what is happening here?

  • The first 2 lines are pretty clear, I think. We declare an array of numbers and create a transducer function which is just a composed function of:
    • R.map(R.add(1)): map over an array and add 1 to each element
    • R.take(2): take the first 2 elements of an array
    • We use R.compose here so it will perform a right-to-left composition, i.e. first take and then map.
  • The last line is where we are going to use the transduce function. The function accepts 4 arguments:
    • The transducer: function that performs the transformation
    • The iterator: in our case we will append the current value to the accumulator
    • The initial value
    • The list to iterate

When we run this example code the result will be an array of [2, 3]. And that is understandable because in the composed transducer function we:

  • Take the first 2 elements of the array -> [1, 2]
  • Add 1 to each element -> [2, 3]

But now you may ask yourself: What is the difference then with just running the composed transducer function with the numbers array? That will have the same result, right? Yes, it has!

// Only running the transducer function with the numbers array will return the same result
transducer(numbers); //=> [2, 3]

So, why are we using this R.transduce function and not just the composed transducer function? What is the added value of using R.transduce?

Benefits of R.transduce

I found this point confusing at first, but it's pretty simple if you understand. Because the benefit of using transduce is performance 🎉

Transducers are all about replacing multiple trips through the same data with a single one.

So with using R.transduce the composed transducer function will be used in a different optimised way, where the array is only iterated once! We can make that clear by replacing the take with another add and by adding some logs to the transducer function:

const transducer = R.compose(
  R.tap(x => console.log('LEFT')),
  R.tap(x => console.log('ADD 1 to', x)),
  R.map(R.add(1)),
  R.tap(x => console.log('ADD 2 to', x)),
  R.map(R.add(2)),
  R.tap(x => console.log('RIGHT'))
);

Now you will see a difference in output when using the transducer directly and when using it with R.transduce.

transducer(numbers); //=> [4, 5, 6, 7]
// RIGHT
// ADD 2 to [3, 4, 5, 6]
// ADD 1 to [4, 5, 6, 7]
// LEFT

Understandable and as expected:

  • Iterate over the array and add 2
  • Iterate over the array (again!) and add 1

Now, can you guess what R.transduce will output when using our modified transduce function? Because it still had a surprise for me when running it for the first time. Let's see:

R.transduce(transducer, R.flip(R.append), [], numbers); //=> [4, 5, 6, 7]
// LEFT
// ADD 1 to 1
// ADD 2 to 2
// RIGHT

// LEFT
// ADD 1 to 2
// ADD 2 to 3
// RIGHT

// ... and the same for the numbers 3 and 4

What we can see clearly now is that the array is only iterated once. Each element is passed to the transformer function before moving to the next element. So that is the performance benefit we were talking about.

But what you also must notice in the output is that R.transduce is performing the operations left-to-right instead of right-to-left what you would expect when using R.compose. And that is just something you must know about transducers:

Transducers compose in an opposite order.

So, using R.compose with a transducer performs left-to-right and using R.pipe with a transducer performs right-to-left. The exact opposite when using normal function composition.

Conclusion

Reading the Ramda docs about R.transduce can be very confusing at first. But in the end it is not that difficult to understand what the function does. At least, I hope you feel the same after reading this post.

Transducers can just be very useful if you need to combine a number of operations (map, take, filter) over a list. With a transducer it's then possible to optimize this process and keep your code clean.

If you still have any questions about transducers, don't hesitate 😄

Cheers

Discussion

pic
Editor guide