JS Journey into outer space - Day 1

This article was written as part of a knowledge sharing program I created for front-end developers. It builds up to creating reusable abstractions by studying async transducers.

Day 1

The God pattern?

Sometimes you have an array that you need filter before you can transform the items. We usually do this with the built-in methods of filter and map.

const input = [1, 2, 3, 4, 5, 6];
const result = input.filter(c => c % 2 === 0).map(c => c * 2);
console.log(result); // [4, 8, 12]

However, when dealing with large arrays chaining multiple methods can become a performance bottleneck: each method applied means another loop.

To improve the performance there we could of course stop using filter and map. There are imperative ways to prevent the additional iterations. However, it's also possible to use a single built-in array method: reduce. Moreover, reduce can be seen as underlying all other operations that can be done on an array or any "container" type, as we'll see later on.

🦉 Using functional instead of imperative programming has many benefits, such as predictability and transparent typing. See for instance this article on Coding Dojo.

How does reduce work again? It takes an operator function and an initial value. The operator function takes an accumulated value, the current item in the array and the index of that item.

🦉 In the case of transforming from array to array that initial value is always an empty array.

const input = [1, 2, 3, 4, 5, 6];
const result = input.reduce((a, c) => c % 2 === 0 ? a.concat([c * 2]) : a, []);
console.log(result); // [4, 8, 12]

Robots are coming

The above example shows how reduce can be used to both filter and transform items in an array. The downside of this code is that it's harder to read, because it doesn't distinguish between the filter part and the map part of the operation.

Is there a way to improve that? It turns out there is. A pattern was introduced in Clojure that was called transducers and soon found its way into JavaScript.

In transducers, function composition is used to create a single operation that can be passed to a reduce function. See the below code snipped from Eric Elliot's nice writeup on transducers.

const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
const map = f => step => (a, c) => step(a, f(c));
const filter = predicate => step => (a, c) => predicate(c) ? step(a, c) : a;
const isEven = n => n % 2 === 0;
const double = n => n * 2;
const doubleEvens = compose(filter(isEven), map(double));
const arrayConcat = (a, c) => a.concat([c]);
const xform = doubleEvens(arrayConcat);
const input = [1, 2, 3, 4, 5, 6];
const result = input.reduce(xform, []);
console.log(result); // [4, 8, 12]

🦉 Note that new code snippets may use functions already defined.

Mechanically pulled 🐥

The above example applies to arrays, but what if we want to extend it to other types at some point? We need a way to get values from the array that is generic enough as to apply it to container types. One such pattern is the generator function. To write a generator function for any array is straightforward enough. Only, when we want to add types we need to use generics, since the array can contain any type.

🦉 Generics is a way to create an interface that can work with a variety of types, while still constraining it more that with using any. Instead one or more "type parameters" are expected. See the page on Generics in the TypeScript handbook.

function* arrayGenerator<T>(arr: T[]) {
  for(const x of arr) yield x;
}

Then we create a transduce function that is similar to reduce, but takes the generator as input. A reduce function iterates over some container and applies the function with the initial value and the current value. The initial value will be accumulated with each iteration and returned at the end.

function transduce(input, fn, generator, init) {
  const source = generator(input);
  let cur = source.next();
  do {
    init = fn(init, cur.value);
    cur = source.next();
  } while (!cur.done);
  return init;
};

Finally we pass the arguments to the transduce function. There are of course other ways to achieve the same and this is just a first step in a process to get to a more generic function.

const xform = doubleEvens(arrayConcat);
const input = [1, 2, 3, 4, 5, 6];
const result = transduce(input, xform, arrayGenerator, []);
console.log(result); // [4, 8, 12]

This concludes the first day 😎

Comments

Popular posts from this blog

JS Journey into outer space - Day 5

Abandoning hope... and XForms

JS Journey into outer space - Day 4