Forever Functional: Maximize JavaScript's Performance WITHOUT Transducers
Published: 2023-12-18 12:39:50

by [Federico Kereki](https://blog.openreplay.com/authors/federico-kereki)


<blockquote><em>
In the [previous article on transducers](https://medium.com/stackanatomy/forever-functional-maximize-javascripts-performance-with-transducers-f0a866d000c0) we saw a way to optimize long sequences of operations on arrays, but the solution wasn't that trivial. Many readers wondered whether we could achieve the same result more simply, and this article will tackle that: we won't use transducers (as we did before) but with the help of some functional programming, we'll get equally good solutions.
</em></blockquote>


<div style="background-color:#efefef; border-radius:8px; padding:10px; display:block;">
<hr/>
<h3><em>Session Replay for Developers</em></h3> 
<p><em>Uncover frustrations, understand bugs and fix slowdowns like never before with <strong><a href="https://github.com/openreplay/openreplay" target="_blank">OpenReplay</a></strong> — an open-source session replay suite for developers. It can be <strong>self-hosted</strong> in minutes, giving you complete control over your customer data.</em></p>
<img alt="OpenReplay" style="margin-top:5px; margin-bottom:5px;" width="768" height="400" src="https://raw.githubusercontent.com/openreplay/openreplay/main/static/openreplay-git-hero.svg" class="astro-UXNKDZ4E" loading="lazy" decoding="async">
<p><em>Happy debugging! <a href="https://openreplay.com" target="_blank">Try using OpenReplay today.</a></em><p>
<hr/>
</div>


Let's remember our original problem -- which was a trivial example, meant to highlight the issues we had. We had an array, and we wanted to (1) take only odd values, (2) duplicate them, (3) keep values under 50, and (4) add 3 to them. The following code achieved this:

```javascript
const testOdd = (x) => x % 2 === 1;
const duplicate = (x) => x + x;
const testUnderFifty = (x) => x < 50;
const addThree = (x) => x + 3;

const myArray = [22, 9, 60, 24, 11, 63];

const newArray = myArray
  .filter(testOdd)
  .map(duplicate)
  .filter(testUnderFifty)
  .map(addThree``
console.log(newArray); // [21,25]
```

What's the problem here? Every time you apply an operation, a new intermediate array is created, and with large amounts of data, the extra time may be significant.

![image](https://blog.openreplay.com/images/forever-functional-maximize-javascript-performance-without-transducers/images/noTransducersImage.png)

In the previous article, we described a solution based on trying to go array element by array element and applying all transformations in order, and we developed transducers for that. But we may make do with something simpler! Let's see that.

## A first (but not too good) solution

Our example was straightforward, and some readers objected by reasoning that it was pretty clear that the only values of the original array that we'd pick would be odd ones, under 25, and then we would duplicate them and add three, so a simple loop could do the job... and we wouldn't even need the original four functions we used!

```javascript
const simplifiedPass = (someArray) => {
  const result = [];
  someArray.forEach((value) => {
    if (value % 2 && value < 25) {
      result.push(value * 2 + 3);
    }
  });
  return result;
};
const newArray2 = simplifiedPass(myArray);
console.log(newArray2);
```

This works and is perfectly correct -- but it's hard to generalize. With our simple example, deducing which numbers would be processed was easy, but in real life, the logical checks could be very complex, and you wouldn't be able to reduce them to a single test as we did here. So, while this shows that optimization is doable, the method itself cannot be applied in every case; we need a more general solution.

## A second (also handcrafted) solution

As in the previous case, we can do better if we go element by element and apply maps and filters in sequence. 

```javascript
const singlePassFourOps = (someArray) => {
  const result = [];
  someArray.forEach((value) => {
    let ok = false; // ①
    if (testOdd(value)) {
      value = duplicate(value);
      if (testUnderFifty(value)) {
        value = addThree(value);
        ok = true;
      }
    }
    if (ok) { // ②
      result.push(value);
    }
  });
  return result;
};

const newArray3 = singlePassFourOps(myArray);
console.log(newArray3); // [ 21,25 ]
```

We use an `ok` variable ① to learn if a `value` from the original array passed all tests ②. Each `filter()` is transformed into an `if`, and each `map()` updates `value`. If we reach the end of the operations, we set `ok` to true, which means we'll push the final value into the `result` array. This works! However, we had to write a function from scratch, and we may be able to achieve a more general solution.

## A third (and more general) solution

The previous code requires writing a function for each set of transformations. However, what about passing all the mapping and filtering functions in an array? This would mean we could write a general function that could deal with any combination of filters and maps.

There's a problem, though: how will we know whether a function in the array is meant to be a filter or a map? Let's have an array with pairs of values: first an "M" or "F", meaning "map" or "filter", and then the function itself. Testing the first value of the pair will let us know what to do with the second value, the function.

```javascript
const singlePassManyOps = (someArray, fnList) => {
  const result = [];
  someArray.forEach((value) => {  // ①
    if (
      fnList.every(([type, fn]) => {  // ②
        if (type === "M") { // ③
          value = fn(value);
          return true;
        } else {
          return fn(value); // ④
        }
      })
    ) {
      result.push(value);
    }
  });
  return result;
};

const newArray4 = singlePassManyOps(myArray, [
  ["F", testOdd],
  ["M", duplicate],
  ["F", testUnderFifty],
  ["M", addThree],
]);
console.log(newArray4);
```

This works: we go through the array, element by element ①. For each function in the array ②, if it's a map ③ we update `value`, and otherwise we test whether to go on or not ④. Why are we using `every()` instead of `foreach()`? The key is that we want to stop going through the array of functions as soon as a filter fails, but `foreach()` doesn't have a nice way to stop the loop. On the other hand, `every()` stops looping as soon as a false result is encountered -- which will happen if a filtering function returns false. (And, by the way, that's why we `return true` after a mapping, so `every()` won't stop.) This is going better! Can we make it simpler?


<CTA_Middle_Programming />



## A fourth (and simpler) solution

We can simplify the work by writing a pair of functions to help us produce the needed array; given a function, they will produce the right pair of values.

```javascript
const applyMap = (fn) => ["M", fn];
const applyFilter = (fn) => ["F", fn];
```

With these functions, we can change the code above to the following.

```javascript
const newArray5 = singlePassManyOps(myArray, [
  applyFilter(testOdd),
  applyMap(duplicate),
  applyFilter(testUnderFifty),
  applyMap(addThree),
]);
console.log(newArray5);
```

This is nicer, and also declarative in aspect: you clearly see we first apply a filter (`testOdd`), then a map (`duplicate`), etc., in the same sequence as in our original code. Also, performance is optimal; we don't have any intermediate arrays, and we don't perform any unnecessary filters or maps. We're done! However...

## The fifth (and definitive) solution

In the previous article, we mentioned we wouldn't always want to finish by producing an array; we could want to apply a final `reduce()` operation. We can fix this by making the initial value of `result` and the function that updates it optional, with predefined default values.

```javascript
const singlePassMoreGeneral = (
  someArray,
  fnList,
  initialAccum = [],
  reduceAccumFn = (accum, value) => {
    accum.push(value);
    return accum;
  }
) => {
  let result = initialAccum;      // ①
  someArray.forEach((value) => {
    if (
      fnList.every(([type, fn]) => {
        if (type === "M") {
          value = fn(value);
          return true;
        } else {
          return fn(value);
        }
      })
    ) {
      result = reduceAccumFn(result, value); // ②
    }
  });
  return result;
};

const newArray6 = singlePassMoreGeneral(myArray, [
  applyFilter(testOdd),
  applyMap(duplicate),
  applyFilter(testUnderFifty),
  applyMap(addThree),
]);
console.log(newArray6);
```

Apart from adding `initialAccum` (the initial value of the accumulator for the reducing operation) and `reduceAccumFn` (the reducer function itself) the other changes are the initial value for `result` ① and the way `result` is updated ②. We can test this by finding the sum of the final values.

```javascript
const newValue = singlePassMoreGeneral(
  myArray,
  [
    applyFilter(testOdd),
    applyMap(duplicate),
    applyFilter(testUnderFifty),
    applyMap(addThree),
  ],
  0,
  (x, y) => x + y
);
console.log(newValue);
```

This worked! We can now apply any sequence of mapping and filtering operations optimally, optionally finishing with a reducing step if desired.

## Summary

We started the article trying to find a simpler, though equally powerful, way of optimizing a sequence of operations on a large array. We went from a very simple solution, which wasn't general enough, to a final solution that's fully equivalent to the transducers' one, although arguably easier to understand, despite also using functional programming in many ways. This goes to show the validity of the idea that "there's more than one way to skin a cat"- or, in our case, "to optimize array processes"!