Search code examples
javascriptaverageramda.jstransducer

Ramda.js transducers: average the resulting array of numbers


I'm currently learning about transducers with Ramda.js. (So fun, yay! 🎉)

I found this question that describes how to first filter an array and then sum up the values in it using a transducer.

I want to do something similar, but different. I have an array of objects that have a timestamp and I want to average out the timestamps. Something like this:

const createCheckin = ({
  timestamp = Date.now(), // default is now
  startStation = 'foo',
  endStation = 'bar'
} = {}) => ({timestamp, startStation, endStation});

const checkins = [
  createCheckin(),
  createCheckin({ startStation: 'baz' }),
  createCheckin({ timestamp: Date.now() + 100 }), // offset of 100
];

const filterCheckins = R.filter(({ startStation }) => startStation === 'foo');
const mapTimestamps = R.map(({ timestamp }) => timestamp);

const transducer = R.compose(
  filterCheckins,
  mapTimestamps,
);

const average = R.converge(R.divide, [R.sum, R.length]);

R.transduce(transducer, average, 0, checkins);
// Should return something like Date.now() + 50, giving the 100 offset at the top.

Of course average as it stands above can't work because the transform function works like a reduce.

I found out that I can do it in a step after the transducer.

const timestamps = R.transduce(transducer,  R.flip(R.append), [], checkins);
average(timestamps);

However, I think there must be a way to do this with the iterator function (second argument of the transducer). How could you achieve this? Or maybe average has to be part of the transducer (using compose)?


Solution

  • I'm afraid this strikes me as quite confused.

    I think of transducers as a way of combining the steps of a composed function on sequences of values so that you can iterate the sequence only once.

    average makes no sense here. To take an average you need the whole collection.

    So you can transduce the filtering and mapping of the values. But you will absolutely need to then do the averaging separately. Note that filter then map is a common enough pattern that there are plenty of filterMap functions around. Ramda doesn't have one, but this would do fine:

    const filterMap = (f, m) => (xs) =>
      xs .flatMap (x => f (x) ? [m (x)] : [])
    

    which would then be used like this:

    filterMap (
      propEq ('startStation', 'foo'), 
      prop ('timestamp')
    ) (checkins)
    

    But for more complex sequences of transformations, transducers can certainly fit the bill.


    I would also suggest that when you can, you should use lift instead of converge. It's a more standard FP function, and works on a more abstract data type. Here const average = lift (divide) (sum, length) would work fine.