Reducing Collections

node v10.24.1
version: 2.0.0
endpointsharetweet
Functional programming without side-effects is a great way of ensuring your code is deterministic: that it will run the same each time, regardless of environment. Let's look at a simple attempt to aggregate data from the Star Wars API, https://swapi.co/!
const getJSON = require("async-get-json") const starWarsPeople = (await getJSON("https://swapi.co/api/people/")).results
We could use a simple loop to collect what we need:
let totalHeight = 0 for (let i = 0; i < starWarsPeople.length; ++i) { totalHeight += (+starWarsPeople[i].height) } totalHeight
That's fine, but the total is mutable; it can be changed after the loop, which is bad! Using reduce, this can be aggregated using a function and returned in one go, meaning it can be assigned to a constant value:
const constTotalHeight = starWarsPeople.reduce((aggregate, datum) => aggregate + (+datum.height), 0) totalHeight === constTotalHeight
Multiple operations can take place within a single reduce, simply by using an object to record what we need and returning it at the end of the function:
const Stopwatch = require("timer-stopwatch") const reduceTimer = new Stopwatch() reduceTimer.start() const stats = starWarsPeople.reduce( (aggregate, datum) => { aggregate.total += (+datum.height) aggregate.mean = aggregate.total / ++aggregate.count return aggregate }, { count: 0, mean: 0, total: 0 }) reduceTimer.stop() const reduceTime = reduceTimer.ms
Compare this with multiple projections and aggregations using the excellent math.js library:
const math = require("mathjs") const timer = new Stopwatch() timer.start() const heights = starWarsPeople.map(datum => datum.height) const mean = math.mean(heights) const total = math.sum(heights) timer.stop() const numberPeople = starWarsPeople.length const projectionTime = timer.ms console.log( "People: " + numberPeople, "Reduce time: " + reduceTime, "Projection time (ms): " + projectionTime) stats.count === numberPeople && stats.mean === mean && stats.total === total && projectionTime > reduceTime
Although slower, the multiple projections are simpler to read and understand here, so unless your data set is huge, they may be worth starting with, then changing based on usage metrics, or as requirements change. You can, of course, split out your aggregation functions, to make them simpler to read and unit test 🔬 These stats are probably a little useless, but feel free to use them by hitting the endpoint!
module.exports.endpoint = (_, res) => res.end(JSON.stringify(stats))
Loading…

no comments

    sign in to comment