## Functional pattern: flatMap

HecateDK · 2017-05-26推荐 · 105阅读 CET/4 106 CET/6 7 原文链接

In this blog post, we look at the operation `flatMap`, which is similar to the Array method `map()`, but more versatile.

## `flatMap()`#

Both `map()` and `flatMap()` take a function `f` as a parameter that controls how an input Array is translated to an output Array:

• With `map()`, each input Array element is translated to exactly one output element. That is, `f` returns a single value.

• With `flatMap()`, each input Array element is translated to zero or more output elements. That is, `f` returns an Array of values.

This is an implementation of `flatMap()`:

``````function flatMap(arr, mapFunc) {
const result = [];
for (const [index, elem] of arr.entries()) {
const x = mapFunc(elem, index, arr);
// We allow mapFunc() to return non-Arrays
if (Array.isArray(x)) {
result.push(...x);
} else {
result.push(x);
}
}
return result;
}
``````

`flatMap()` is simpler if `mapFunc()` is only allowed to return Arrays, but we don’t impose this restriction here, because non-Array values are occasionally useful (see the section on `flatten()` for an example).

To demonstrate how `flatMap()` works, we use the helper function `fillArray`:

``````function fillArray(x) {
return new Array(x).fill(x);
}
``````

This is `fillArray()` in action:

``````> fillArray(1)
[ 1 ]
> fillArray(2)
[ 2, 2 ]
> fillArray(3)
[ 3, 3, 3 ]
``````

This is `flatMap()` in action:

``````> flatMap([1,2,3], fillArray)
[ 1, 2, 2, 3, 3, 3 ]
``````

What is `flatMap()` good for? Let’s look at use cases!

## Use case: filtering and mapping at the same time #

The result of the Array method `map()` always has the same length as the Array it is invoked on. That is, its callback can’t skip Array elements it isn’t interested in.

The ability of `flatMap()` to do so is useful in the next example: `processArray()` returns an Array where each element is either a wrapped value or a wrapped error.

``````function processArray(arr, process) {
return arr.map(x => {
try {
return { value: process(x) };
} catch (e) {
return { error: e };
}
});
}
const results = processArray(myArray, myFunc);
``````

`flatMap()` enables us to extract just the values or just the errors from `results`:

``````const values = flatMap(results,
result => result.value ? [result.value] : []);
const errors = flatMap(results,
result => result.error ? [result.error] : []);
``````

## Use case: mapping to multiple values #

The Array method `map()` maps each input Array element to one output element. But what if we want to map it to multiple output elements?

That becomes necessary in the following example: The React component `TagList` is invoked with two attributes.

``````<TagList tags={['foo', 'bar', 'baz']}
handleClick={x => console.log(x)} />
``````

The attributes are:

• An Array of tags, each tag being a string.

• A callback for handling clicks on tags.

`TagList` is rendered as a series of links separated by commas:

``````class TagList extends React.Component {
render() {
const {tags, handleClick} = this.props;
return flatMap(tags,
(tag, index) => [
...(index > 0 ? [', '] : []), // (A)
<a key={index} href=""
onClick={e => handleClick(tag, e)}>
{tag}
</a>,
]);
}
}
``````

In line A, we are conditionally inserting the Array element `', '` via the spread operator (`...`). This trick is explained in another blog post.

Due to `flatMap()`, `TagList` is rendered as a single flat Array. The first tag contributes one element to this Array (a link); each of the remaining tags contributes two elements (comma and link).

## Other versions of `flatMap()`#

### Arbitrary iterables #

`flatMap()` can be generalized to work with arbitrary iterables:

``````function* flatMapIter(iterable, mapFunc) {
let index = 0;
for (const x of iterable) {
yield* mapFunc(x, index);
index++;
}
}
``````

`flatMapIter()` function works with Arrays:

``````function fillArray(x) {
return new Array(x).fill(x);
}
console.log([...flatMapIter([1,2,3], fillArray)]); // (A)
// [1, 2, 2, 3, 3, 3]
``````

In line A, we translate the iterable returned by `flatMapIter()` into an Array, via the spread operator (`...`).

One nice trait of `flatMapIter()` is that it works incrementally: as soon as the first input value is available, output is produced. In contrast, the Array-based `flatMap()` needs all of its input to produce its output.

That can be demonstrated via the infinite iterable created by the generator function `naturalNumbers()`:

``````function* naturalNumbers() {
for (let n=0;; n++) {
yield n;
}
}
const infiniteInput = naturalNumbers();
const infiniteOutput = flatMapIter(infiniteInput, fillArray);
const [a,b,c,d,e] = infiniteOutput; // (A)
console.log(a,b,c,d,e);
// 1 2 2 3 3
``````

In line A, we extract the first 5 values of `infiniteOutput` via destructuring.

### Implementing `flatMap()` via `reduce()`#

You can use the Array method `reduce()` to implement a simple version of `flatMap()`:

``````function flatMap(arr, mapFunc) {
return arr.reduce(
(prev, x) => prev.concat(mapFunc(x)),
[]
);
}
``````

I prefer the original version of `flatMap()`, because it is easier to understand.

`flatten()` is an operation that concatenates all the elements of an Array:

``````> flatten(['a', ['b','c'], ['d']])
[ 'a', 'b', 'c', 'd' ]
``````

It can be implemented as follows:

``````const flatten = (arr) => [].concat(...arr)
``````

Using `map()` and flattening the result is the same as using `flatMap()`. That is, the following two expressions are equivalent:

``````flatten(arr.map(func))
flatMap(arr, func)
``````

Similarly, using `flatMap()` with the identity function (`x => x`) is the same as using `flatten()`. That is, the following two expressions are equivalent:

``````flatMap(arr, x => x)
flatten(arr)
``````

The next subsections cover use cases for `flatten()`.

### Use case: conditionally inserting values into an Array #

The following code only inserts `'a'` if `cond` is `true`:

``````const cond = false;
const arr = flatten([
(cond ? ['a'] : []),
'b',
]);
// ['b']
``````

For more information on conditionally inserting Array elements, consult the blog post “Conditionally adding entries inside Array and object literals”.

### Use case: filtering out failures #

In the following example, `downloadFiles()` only returns the texts that could be downloaded.

``````async function downloadFiles(urls) {
}
try {
const response = await fetch(url);
const text = await response.text();
return [text]; // (C)
} catch (err) {
return []; // (D)
}
}
``````

`downloadFiles()` first maps each URL to a Promise resolving to either:

• An empty Array (line D)

`Promises.all()` (line A) converts the Array of Promises into a Promise that resolves to a nested Array. `await` (line A) unwraps that Promise and `flatten()` un-nests the Array (line B).

Note that we couldn’t have used `flatMap()` here, because of the barrier imposed by the Promises returned by `downloadFile()`: when it returns a value, it doesn’t know yet if it will be a text or an empty Array.

## An ECMAScript proposal for `Array.prototype.flatMap()`#

Brian Terlson has written a proposal for an Array method `flatMap()`. Alas, it’s currently at stage −1. More work is needed: it might make more sense as a method for iterables (or a function operating on them), etc.