ponyfoo.com

Proposal Draft for .flatten and .flatMap

Fix
A relevant ad will be displayed here soon. These ads help pay for my hosting.
Please consider disabling your ad blocker on Pony Foo. These ads help pay for my hosting.
You can support Pony Foo directly through Patreon or via PayPal.

Array prototype may be getting .flatten and .flatMap methods may be coming to ECMAScript in a distant future. This article describes what the proposal holds in store.

A very early draft was published last week by the ECMAScript editor Brian Terlson. When I say very early I mean it’s considered a “stage -1” proposal, meaning it’s not even a formal proposal yet, just a very early draft.

That being said, I’m always excited about new Array.prototype methods so I decided to write an article nonetheless. These kinds of methods were popularized in JavaScript by libraries like Underscore and then Lodash – and some of them – such as .includes, have eventually started finding their way into the language.

Shall we take a look?

A car compactor
A car compactor

Array.prototype.flatten

The .flatten proposal will take an array and return a new array where the old array was flattened recursively. The following bits of code represent the Array.prototype.flatten API.

[1, 2, 3, 4].flatten() // <- [1, 2, 3, 4]
[1, [2, 3], 4].flatten() // <- [1, 2, 3, 4]
[1, [2, [3]], 4].flatten() // <- [1, 2, 3, 4]

One could implement a polyfill for .flatten thus far like below. I separated the implementation of flatten from the polyfill so that you don’t necessarily have to use it as a polyfill if you just want to use the method without changing Array.prototype.

Array.prototype.flatten = function () {
  return flatten(this)
}
function flatten (list) {
  return list.reduce((a, b) => (Array.isArray(b) ? a.push(...flatten(b)) : a.push(b), a), [])
}

Keep in mind that the code above might not be the most efficient approach to array flattening, but it accomplishes recursive array flattening in a few lines of code. Here’s how it works.

  • A consumer calls x.flatten()
  • The x list is reduced using .reduce into a new array [] named a
  • Each item b in x is evaluated through Array.isArray
  • Items that aren’t an array are pushed to a
  • Items that are an array are flattened into a new array
  • This eventually results in a flat array

The proposal also comes with an optional depth parameter – that defaults to Infinity which can be used to determine how deep the flattening should go.

[1, [2, [3]], 4].flatten() // <- [1, 2, 3, 4]
[1, [2, [3]], 4].flatten(2) // <- [1, 2, 3, 4]
[1, [2, [3]], 4].flatten(1) // <- [1, 2, [3], 4]
[1, [2, [3]], 4].flatten(0) // <- [1, [2, [3]], 4]

Adding the depth option to our polyfill wouldn’t be that hard, we pass it down to recursive flatten calls and ensure that, when the bottom is reached, we stop flattening and recursion.

Array.prototype.flatten = function (depth=Infinity) {
  return flatten(this, depth)
}
function flatten (list, depth) {
  if (depth === 0) {
    return list
  }
  return list.reduce((accumulator, item) => {
    if (Array.isArray(item)) {
      accumulator.push(...flatten(item, depth - 1))
    } else {
      accumulator.push(item)
    }
    return accumulator
  }, [])
}

Alternatively – for Internet points – we could fit the whole of flatten in a single expression.

function flatten (list, depth) {
  return depth === 0 ? list : list.reduce((a, b) => (Array.isArray(b) ?
    a.push(...flatten(b, depth - 1)) :
    a.push(b), a), [])
}

Then there’s .flatMap.

Array.prototype.flatMap

This method is convenient because of how often use cases come up where it might be appropriate, and at the same time it provides a small boost in performance, as we’ll note next.

Taking into account the polyfill we created earlier for flattening through Array.prototype.flatten, the .flatMap method can be represented in code like below. Note how you can provide a mapping function fn and its ctx context as usual, but the flattening is fixed at a depth of 1.

Array.prototype.flatMap = function (fn, ctx) {
  return this.map(fn, ctx).flatten(1)
}

Typically, the code shown above is how you would implement .flatMap in user code, but the native .flatMap trades a bit of readability for performance, by introducing the ability to map items directly in the internal flatten procedure, avoiding the two-pass that’s necessary if we first .map and then .flatten an Array.

A possible example of using .flatMap can be found below.

[{ x: 1, y: 2 }, { x: 3, y: 4 }, { x: 5, y: 6 }].flatMap(c => [c.x, c.y])
// <- [1, 2, 3, 4, 5, 6]

The above is syntactic sugar for doing .map(c => [c.x, c.y]).flatten() while providing a small performance boost by avoiding the aforementioned two-pass when first mapping and then flattening.

Note that our previous polyfill doesn’t cover the performance boost, let’s fix that by changing our own internal flatten function and adjust Array.prototype.flatMap accordingly. We’ve added a couple more parameters to flatten, where we allow the item to be mapped into a different value right before flattening, and avoiding the extra loop over the array.

Array.prototype.flatMap = function (fn, ctx) {
  return flatten(this, 1, fn, ctx)
}
function flatten (list, depth, mapperFn, mapperCtx) {
  if (depth === 0) {
    return list
  }
  return list.reduce((accumulator, item, i) => {
    if (mapperFn) {
      item = mapperFn.call(mapperCtx || list, item, i, list)
    }
    if (Array.isArray(item)) {
      accumulator.push(...flatten(item, depth - 1))
    } else {
      accumulator.push(item)
    }
    return accumulator
  }, [])
}

Since the mapperFn and mapperCtx parameters of flatten are entirely optional, we could still use this same internal flatten function to polyfill both .flatten and .flatMap.

Liked the article? Subscribe below to get an email when new articles come out! Also, follow @ponyfoo on Twitter and @ponyfoo on Facebook.
One-click unsubscribe, anytime. Learn more.

Comments