Flatten Arrays in Vanilla JavaScript with Flat() and FlatMap()
wisdomgeek.com
wisdomgeek.com
[1, 2, 3, 4, 5].filter(n => n % 2 === 1).map(n => n * 2)
You can do: [1, 2, 3, 4, 5].flatMap(n => n % 2 === 1 ? [n * 2] : [])
Again, this is a contrived example, but I think it's interesting since the generality is not obvious (to me)> Note, however, that this is inefficient and should be avoided for large arrays: in each iteration, it creates a new temporary array that must be garbage-collected, and it copies elements from the current accumulator array into a new array instead of just adding the new elements to the existing array.
I personally do actually use flatMap sometimes as outlined in your example - perf is an after issue and I'll refactor if the concise code isn't worth the cost.
That note is misleading.
> It's the most straightforward way other developers will understand what's going on.
… has been the opposite of my experience. Both on the job (where I’ve always conceded to team preference for imperative loops) and observing the community (hating on reduce is a whole meme on JavaScript/TypeScript Twitter, and the contrary meme has never shown up at least on my feed).
And I honestly understand why it’s not very popular. Reduce/fold is a very FP concept which isn’t particularly idiomatic in real world JS. When I learned and embraced it (myself coming from a JS background), it took dozens of real uses before I felt like I had committed to my own memory what’s actually happening. And by then I think I was writing Clojure.
There are definitely operations that are more intuitive as reduces, but any sequences consisting of exclusively filter, map, and/or flatMap are, IME and IMO, about the worst candidates.
(That said, because the JS filter, map, etc. operations are eager rather than lazy, sequenced operations produce intermediate arrays that may be undesirable especially with large datasets, so reduce can be desirable even without being more clear in intent.)
In TypeScript, you might have an array of multiple types (e.g. `Array<A | B>`), and use a `filter` call to only keep the `A`s. However, in many situations TypeScript can't figure this out and the resulting array type is still `Array<A | B>`. However, when you just use `flatMap` to do nothing more than filtering in the same way, TypeScript can determine that the resulting type is just `Array<A>`. It's a bit unfortunate really - `filter` is faster and more readable, but the ergonomics of `flatMap` type-wise are so much nicer! Just some interesting trivia.
[0]: https://github.com/microsoft/TypeScript/issues/16069#issueco...
You could potentially add a syntax for type guards function types, then add a signature to filter that accepts a type guard and returns an array of the guarded types.
Shouldn't be too much of a stretch given that we have type guards.
The syntax is a bit annoying... should be something like filter<A, B>(cb: A => A is B)
:/
I don't know why the type isn't narrowed in Array.filter like it is in if statements without this weird workaround.
const array: (number | string)[] = [];
const mixedArray = array.filter(value => typeof value === 'string');
// mixedArray: (number | string)[]
const arrayOfString = array.filter((value): value is string => typeof value === 'string');
// arrayOfString: string[]
This example in Typescript playground: https://www.typescriptlang.org/play?#code/MYewdgzgLgBAhgJwXA...[1]: https://www.typescriptlang.org/docs/handbook/advanced-types....
filter<U extends T>(pred: (a: T) => a is U): U[];
Additionally, getting TS better at inferring type guards is an open issue (literally): https://github.com/microsoft/TypeScript/issues/38390
ls.flatMap(x => {
if (x < 0) {
return []
} else if (x == 0) {
return [0]
} else {
return [Math.sqrt(x), -Math.sqrt(x)]
}
})
gives you all the real square roots from the original list, doing the mapping, flattening, and filtering all in one function call. ls.filter(o=>0<o).map(o=>o||[Math.sqrt(x, -Math.sqrt(x)])nothing, but they do have some relationship to 0, "" and Promise.resolve() - the array is handling the logic that will make the results be combined, not the doubling part
You do have to make sure that your implementation of list is extremely efficient on zero and one element lists (ideally it generates no garbage at all in those cases) otherwise as other commentators have pointed out you'll have a lot of GC pressure.
And even though the transducer itself is `x -> List(x)` note that the `List` is only produced as an intermediate step and doesn't need to exist in the final product. You could apply a `x -> List(x)` to a generator for example and just "absorb" the list back into the resulting generator.
Or to put it another way, if I reviewed code where someone used flatMap for anything other than lists of lists I'd be likely to suggest filter/map or reduce or some other convenient equivalent depending on the purpose of the code.
Something like Ruby's filter_map[0] would do the job, although not with this particular example (because 0 is truthy in Ruby).
[0] https://ruby-doc.org/core-3.1.0/Enumerable.html#method-i-fil...
/** Return this symbol to skip the current value. */
const SKIP = Symbol("mapFilter.SKIP");
/**
* @template T, R
* @param {T[]} array
* @param {(SKIP: Symbol, currentValue: T, index: number, array: T[]) => R|SKIP} callback return `SKIP` to filter out an element
* @param {number} [begin] defaults to 0
* @param {number} [end] defaults to `array.length`
* @returns {R[]}
*/
function mapFilter(array, callback, begin = 0, end = array.length) {
const ret = [];
for (let i = begin; i < end; i++) {
const v = callback(SKIP, array[i], i, array);
if (v !== SKIP) ret.push( /** @type {R} */ (v));
}
return ret;
};
Here. Less than ten lines without JSDoc type annotations, twenty with them. It lets you slice, map and filter all in one call without allocating intermediate arrays like you would when chaining them, making it almost as fast as a plain for-loop. It's also easy to turn it into an in-place version, removing even the array allocation overhead. [1, 2, 3, 4, 5].reduce((x, y) => y % 2 === 1 ? [...x, y * 2] : x, []) [1, 2, 3, 4, 5].reduce((x, y) => { if (y % 2 === 1) x.push(y * 2); return x; }, [])
I've many times wished that push() would just return the array, it would make reduce() far easier for this sort of use case. x.concat([y*2])
would return the array (but makes a duplicate)Anyway, I find this to be a whole lot more sensible:
x=[];
for(y of [1,2,3,4,5]){
if(y%2===1)x.push(y*2)
}
Or even! y=[1,2,3,4,5];
x=[];
// map reduce/flatmap/map/filter etc omg wtf
for( i=0; i < y.length; i++ ){
if( y[i]%2 === 1 ){ x.push( y[i] * 2 ); }
}
I cant even tell what language this is but there is nothing here that needs fixing.[1, 2, 3, 4, 5].reduce((acc, n) => n % 2 === 1 ? acc.push(2*n) : acc, [])
(acc.push(2*n), acc)
In an expression position, the push statement will be executed, then its return will be discarded, and the final expression will be the result of the expression. Is it “better”? Almost certainly not. But it lets you stay in expression syntax while executing statements. (Much more useful for logging than meaningful runtime side effects IMO, but I think it should be more widely known in general.)Edit: and I’m glad to see another reference to it down thread!
[1, 2, 3, 4, 5].reduce((acc, n) => n % 2 === 1 ? acc.concat([2*n]) : acc, []) [1, 2, 3, 4, 5].reduce((acc, n) => (n % 2 ? acc.push(2*n) : null, acc), []) let input = [1, 2, 3, 4, 5], output = [];
for (let i = 0; i < input.length; ++i) {
let n = input[i];
if (n % 2) output.push(2*n);
}
return output;
But in some circumstances the other style can be more convenient / legible. The immediate question was about pushing to an array and then returning the array, for which the comma operator can be handy.FWIW, it's 2022:
const output = [];
for (const n of [1, 2, 3, 4, 5]) {
if (n % 2) output.push(2 * n);
}Anyway, I use TypeScript, so if I really want to assert that my array is immutable (as immutable as stuff in JS-land gets anyway) I just write:
const input: readonly number[] = [1, 2, 3, 4, 5];
or even const input = [1, 2, 3, 4, 5] as const; [1, 2, 3, 4, 5].reduce((acc, n) => n % 2 === 1 ? [ ...acc, 2 * n ] : acc, [])I'm often delighted to find new convenience functions I can just use without adding another dependable.
E.g.
sumBy(“orders”, “total.amount”)So I still prefer using lodash map, reduce, filter, etc for that added defensiveness.
Is there some specific reason that `flatMap` made the cut?
Same as `flatmap` (`bind`), `flat` is 'part' of a monad (as `join`).
But in a program it usually makes sense to do the flattening in one go, since it avoids the need to do some possibly expensive intermediate calculations.
I think conceptually, `flatMap` is a `map` where each iteration can return multiple values (or none). So it feels quite more powerful than just `map` and is a quite common convenience function.
But in the real world, if we must have a default, I think it should be 1, not infinity.
Infinity has a frustrating fail state, in my opinion: you might be flattening arrays that weren't meant to be flattened. And now you've got a cluster!@#$ of data to look at and reason about.
The fail state of 1 is that it doesn't flatten enough but you have the original structure in your 1-level flattened output. So you're far more likely to make heads and tails of how much more flattening you need.
For me, I just find that in almost all cases I've used flat it's been with Infinity as the depth value. It seems to me as well that most of the time you'd probably either want 1 or Infinity, not 5 or 12 or whatever, so perhaps it would've even been better to just have two functions: flat and flatDeep (or some such, naming is hard) the latter of which would default to Infinity but allow different depths as well.
Probably reads better than just flat as well, e.g. `.flatDeep(5)` rather than `.flat(5)`. Oh well, we're stuck with this now so it's all an academic exercise anyway, but I'd be curious to know the rationale of the design. Maybe I'll wade through the spec repo one day to see if there's any discussion about it.
I've been a developer for 15 years and I have rarely found a need for flatMap.
Example in the article is meaningless to me:
array.map(x => [x \* 2]);
// [[2], [4], [6], [8]]
array.flatMap(x => [x * 2]);
// [2, 4, 6, 8]
because the right way would be array.map(x => x *2); anyway.What am I missing? What is a realistic scenario where an array needs to be flattened?
Concatenating the result of a paginated API.
Showing all the objects two or more 1:N steps away from you in the object graph. The events your friends are attending, the issues your coworkers are working on, the people belonging to any of your same groups. Basically any time you would do a SELECT... JOIN in SQL.
With how many wrappers around paginated APIs to unpaginate them I must be wrong but it still bugs me.
But far more often, the pagination is forced - get 100 results, hit this link for the next 100 - in order to limit the load on the server.
Most trivial scenario, client runs a search that's too generic, you don't want to waste server resources actually preparing 999999 results.
For example: at work we deal with "roles" (professions), and underneath those there can be different specialisms. In some of our views we have filters that users can use to show a subset of the data. There is a filter for roles, and a filter for specialisms. But the specialisms filter should only show options that are relevant given the roles selected in the roles filter. So the code for generating the options to display in the specialisms filter dropdown is something like:
const availableSpecialisms = roles
.filter(role => selectedRoles.includes(role.id))
.flatMap(role => role.specialisms) parents.flatMap(parent => parent.children)It's basically the same as the list monad in haskell, and there are examples here https://en.wikibooks.org/wiki/Haskell/Understanding_monads/L... that can be followed even if you don't know any Haskell.
myFriends: { name: string, friends: friend[] }[]
If I wanted to get a list of friends of friends, I could do something like
myFriends.map(prop('friends')).flatten()
Or
myFriends.flatMap(prop('friends'))
My data is a list of lists but I really want a list of friends so I flatten.
Maybe the Friend object is poorly created/designed but often times you'll just have to deal with whatever the API gives you.
And of course you could call `flatmap` bind, if you prefer ;)
[ { name: "Saransh Kataria" , roles: ['system-admin', 'developer'] }, { name: "Wisdom Geek" , roles: ['basic'] }, ].flatMap(x => x.roles);
// Output => ["admin", "system-admin", "developer"]
So maybe collect is a better idea.