Expressively Selecting a Strategy using ES2016

I find myself needing to select a strategy based on some arbitrary function of input often enough to look for a neat solution. Maybe it’s the output of a remote service that I want to decorate with a summary, or records from a document store that I want to normalize somehow. ES2015’s destructuring, Array find method and arrow functions provide the most flexible, concise and expressive way of choosing the appropriate strategy from a list on a first-match basis that I’ve come up with so far. I’ll be using Babel to transpile Node 4 up to ES2015 spec.

For example, say our spec says that given an input y:
* if it’s a string, uppercase it
* else if it’s an array, return a string describing the length
* else if it’s an object, return a string describing the number of keys
* else return “Nothing Matched” and the default toString() output

We’ll define an array of pairs of functions, where the first element in each pair will be treated like a predicate, and the second will be invoked if the first ‘matches’. Arrow functions make this definition much clearer than the traditional function() {...} syntax.

const renderingStrategies = [
  [x => typeof x === 'string',  x => x.toUpperCase()],
  [x => Array.isArray(x),       x => `Array with ${x.length} elements`],
  [x => typeof x === 'object',  x => `Object with ${Object.keys(x).length} keys`],
  [() => true,                  x => `Nothing matched '${x}'`]
];

That seems fairly expressive to me, mapping pretty directly onto the spec. You could use an array of objects, each with a pair of methods like (match, handle), but that involves quite a bit more boilerplate. Likewise, an if/else-if/else structure could do the job, but it’s more boilerplate and, for me at least, doesn’t imply the intent as clearly.

Now, we need a function that, for an input, selects the first strategy for which the predicate is true. Use array find() to choose the first matching predicate and destructuring to clearly pull out the predicate make this a one-liner.

const render = x => renderingStrategies.find(([matches]) => matches(x))[1](x);

render('Hello World'); // HELLO WORLD
render([1, 2, 3, 4]);  // Array with 4 elements
render({x: 1, y: 2});  // Object with 2 keys
render(1234);          // Nothing matched '1234'

Performance of this selector and its if/elseif/else version are roughly equivalent, both completing a million selections in around a second on my computer. It’s a shame that the only simple way I can see to pull out the decorator function (without a verbose filter and map) is to extract by index. Let me know if you can see a better way!

If we were to use promises, then we could use destructuring again, and make our function asynchronous. For example:

const render = x => Promise.resolve(renderers.find(([matches]) => matches(x)))
  .then(([,decorate]) => decorate(x));

If you can improve on this, or suggest a better solution, leave me a comment, or get me on Twitter.

Author: brabster

Software developer in the North of England

Leave a comment