[HN Gopher] Flatten Arrays in Vanilla JavaScript with Flat() and...
___________________________________________________________________
Flatten Arrays in Vanilla JavaScript with Flat() and FlatMap()
Author : saranshk
Score : 61 points
Date : 2022-01-05 15:18 UTC (7 hours ago)
(HTM) web link (www.wisdomgeek.com)
(TXT) w3m dump (www.wisdomgeek.com)
| vmchale wrote:
| In J this is just , (ravel)
| https://code.jsoftware.com/wiki/Vocabulary/comma
| rak1507 wrote:
| More like ; than ,
| scotty79 wrote:
| A lot of funtional primitives have really counterintuitive names
| for me. flatMap is one of them.
| saranshk wrote:
| agreed, though I doubt there's anything we can do about it.
| chusk3 wrote:
| In F# we call it 'collect' instead, partially for this
| reason.
| scotty79 wrote:
| When I tried to come up with better name for _flatMap_ I
| got _gather_.
|
| So maybe _collect_ is a better idea.
| yawnxyz wrote:
| I've made it a habit to check Mozilla's JS docs once in a while
| for functions like Flat() and FlatMap(). Sometimes if I find
| myself reaching for underscore/lodash, I'll check Mozilla docs
| first to see if there's some new function that can let me omit
| using lodash.
|
| I'm often delighted to find new convenience functions I can just
| use without adding another dependable.
| saranshk wrote:
| there have been a lot of those added to the ES spec lately and
| a lot more coming soon. So it is always good to check MDN
| because I usually end up learning something new mostly.
| copperx wrote:
| Is there a Google equivalent to MDN?
| markstos wrote:
| No.
| newsbinator wrote:
| I often use Lodash's sumBy, groupBy and orderBy. Love the
| versatility and one-liner aspect.
|
| E.g. sumBy("orders", "total.amount")
| johndough wrote:
| Which input method are you using that double quotes are "
| (U+201C) instead of " (U+0022)?
| newsbinator wrote:
| Wow, no clue. That was on mobile... web app or Octal
| (native app). Stock iOS English (US) keyboard.
| Waterluvian wrote:
| I've always found `flatMap` to be interesting because it feels
| like a convenience function, combining .map(fn).flat() into one
| function. It's interesting because JS doesn't really have a lot
| of convenience functions that are this shallow (ie. that provide
| just minimal cleanup compared to the functions they're wrapping).
|
| Is there some specific reason that `flatMap` made the cut?
| beaconstudios wrote:
| probably because flatMap (also called chain) is an important
| array (or monads in general) function in functional
| programming. If anything, .flat() is the aberration.
| ReleaseCandidat wrote:
| > If anything, .flat() is the aberration.
|
| Same as `flatmap` (`bind`), `flat` is 'part' of a monad (as
| `join`).
| beaconstudios wrote:
| good point, I didn't think of that equivalence.
| [deleted]
| contravariant wrote:
| In mathematics it would be 'flat' that is the important one,
| not flatMap. You get 'flatMap' by just composing the monad
| multiplication 'flat' with the monad functor's mapping of
| arrows 'map'. That's mainly just to highlight the similarity
| with regular monoids though.
|
| 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.
| Tuna-Fish wrote:
| For a lot of common cases (where the returned lists from the fn
| are short, but the complete list is long), a combined flatMap
| can be optimized to be much faster than doing the functions one
| by one.
| HellzStormer wrote:
| Small mistake, and maybe that's why this function exists, but
| it's actually combines `.map(fn).flat(1)`.
|
| 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.
| mstade wrote:
| I think the depth argument of flat defaults to 1, so it'd be
| the same anyway. Not sure why the default is 1, I'd prefer
| Infinity so it'd always flatten everything by default, but
| maybe there are some good reasons for it.
| Waterluvian wrote:
| I think it should be explicit without a default.
|
| 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.
| mstade wrote:
| Arguably the same argument works the other way around as
| well, that in thinking flat recursively flattens all
| arrays of arrays you get confused when it doesn't. (I've
| definitely run across that footgun.)
|
| 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.
| jmath wrote:
| perhaps it's a case of "names are fun"
| fathyb wrote:
| My favorite use case for .flatMap() is to define matrix tests for
| CI: // Create 9 jobs export const jobs
| = ['windows', 'macos', 'linux'].flatMap(platform =>
| [12, 14, 16].map(nodeVersion => ({ name: `Test on
| ${platform}/node-${nodeVersion}`, agent: { tags:
| [platform, `node-${nodeVersion}`, steps: ['make
| test'] })) )
| heydenberk wrote:
| Something that people may not see immediately is that flatMap is
| more general than map _and_ filter. Say, for a contrived example,
| that you 'd like to filter out the even numbers in an array, and
| then double the odd numbers that remain. Instead of:
| [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)
| jasonkillian wrote:
| In addition to being essentially a combined "filter" and "map",
| it's also a "better" filter than filter itself in TypeScript in
| such that it narrows types much more ergonomically[0].
|
| 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...
| amitport wrote:
| I wonder if it is possible to add a feature to Typescript to
| help with this:
|
| 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)
|
| :/
| abrioy wrote:
| You can use a type guard[1] as an argument to Array.filter,
| but the function has to be explicitly typed as such.
|
| 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.typescri
| ptlang.org/play?#code/MYewdgzgLgBAhgJwXA...
|
| [1]: https://www.typescriptlang.org/docs/handbook/advanced-
| types....
| nesarkvechnep wrote:
| Or... use reduce.
| Vanit wrote:
| For those wondering at home, the reason you shouldn't do this
| is immediately spelled out in the Mozilla docs for flatMap:
|
| > 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.
| hildjj wrote:
| I just filed this issue on the MDN page:
| https://github.com/mdn/content/issues/11763
|
| That note is misleading.
| edflsafoiewq wrote:
| It still creates a temporary [x, 2*x] array for every
| element though. This is an unavoidable problem with
| flatMap, while reduce can easily be changed to reuse the
| same accumulator array, making it twice as fast as flatMap
| and almost as fast as the simple for-loop approach.
| heydenberk wrote:
| Sure. I'm not suggesting it be used to this effect; I'm
| noting the generality as an interesting point.
| ReleaseCandidat wrote:
| Yes, but you could also use `fold`^H^H^H^H`reduce`.
|
| [1, 2, 3, 4, 5].reduce((acc, n) => n % 2 === 1 ? acc.push(2*n)
| : acc, [])
| pwdisswordfish9 wrote:
| What is `f`reduce`?
| christophilus wrote:
| Push returns the length of the array, though, so that won't
| work.
| ReleaseCandidat wrote:
| Ah, sorry, so you have to concat the arrays using `concat`.
| [1, 2, 3, 4, 5].reduce((acc, n) => n % 2 === 1 ?
| acc.concat([2*n]) : acc, [])
| spiralx wrote:
| Surely the spread operator is nicer here?
| [1, 2, 3, 4, 5].reduce((acc, n) => n % 2 === 1 ? [
| ...acc, 2 * n ] : acc, [])
| v413 wrote:
| This is not efficient. Each iteration creates a new array
| instance due to the spread operator.
| jasonkillian wrote:
| Wouldn't recommend doing this - if the original array is
| of significant length this'll get quite slow because
| `acc.concat` has to create a brand new array of slightly
| longer length on each iteration it's called. Better to
| just use `push` like you suggested before and then return
| the array if you want to use `reduce`.
| ReleaseCandidat wrote:
| Yes, of course, that's why I used `push` at first.
| jacobolus wrote:
| Use the comma operator: (acc.push(2*n), acc) will return
| acc. Or e.g. [1, 2, 3, 4, 5].reduce((acc,
| n) => (n % 2 ? acc.push(2*n) : null, acc), [])
| wk_end wrote:
| If you're just iterating through the array and mutating
| an object on each iteration, just use a for loop.
| jacobolus wrote:
| Obviously you can alternately write: 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.
| wk_end wrote:
| No argument that the comma operator is a neat trick when
| you need it.
|
| FWIW, it's 2022: const output = [];
| for (const n of [1, 2, 3, 4, 5]) { if (n % 2)
| output.push(2 * n); }
| kaba0 wrote:
| It is also much less readable.
| newlisp wrote:
| If you are looking for really general and powerful, then there
| is the mighty reduce: [1, 2, 3, 4,
| 5].reduce((x, y) => y % 2 === 1 ? [...x, y * 2] : x, [])
| femto113 wrote:
| The spread operator looks cool and makes just returning the
| ternary operator work here but its performance implications
| are non-obvious (it's makin' copies). With reduce() you're
| really wanting something like this: [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.
| niek_pas wrote:
| You could do that, but I'd argue using filter then map is more
| readable. What do empty arrays have to do with doubling even
| integers?
| nefitty wrote:
| I agree on the readability. There's a TC proposal floating
| around for a pipeline operator. I don't think it's moved but
| that would be a game changer.
| frozenlettuce wrote:
| >What do empty arrays have to do with doubling even integers
|
| 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
| lalaithion wrote:
| The interesting generalization is that once you realize that
| flatMap lets you map and filter at the same time is that you
| can generate arbitrary elements in the output list
| corresponding to each item in the input list. For example,
| 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.
| 6510 wrote:
| (just for laughs)
| ls.filter(o=>0<o).map(o=>o||[Math.sqrt(x, -Math.sqrt(x)])
| hajile wrote:
| Filter and then map will iterate the list twice. JS really
| needs some iterator-based methods like Lodash where it will
| only go through the list once in this case.
| dwohnitmok wrote:
| You've discovered transducers (which I think have a rather
| horrible and confusing for newcomers higher-order function
| presentation in the language that popularized them, i.e.
| Clojure, when they could just be lists)! All transducers are is
| a function `x -> List(x)` and then you can use other functions
| such as `flatMap` to apply them (as your example illustrates
| nicely this is why map, filter, and its combination can all be
| described as single transducers).
|
| 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.
| [deleted]
| ljm wrote:
| I'm not sure it's any more general considering that you have to
| return an array and also treat an empty array as a 'null'
| value.
|
| 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...
| _greim_ wrote:
| I'm glad JS is adding these methods to Array. It would be nice if
| iterables had similar methods for working with lazy sequences,
| similar to Rust, but this isn't practical since "iterable" is a
| protocol. Array can do it because it's a class. Perhaps JS could
| also adopt something similar to Rust's traits, such that
| implementing the protocol would make a set of related methods
| callable on that object.
| Rygian wrote:
| I have very little experience with arrays in JS, but I always
| have a nagging feeling that, if my algorithm needs me to flatten
| an array, then there is something wrong with the data structures
| I am using.
|
| 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?
| thaunatos wrote:
| Consider a parent -> child relation. You have an array
| `parents`, but want all their children.
| parents.flatMap(parent => parent.children)
| beardedetim wrote:
| Let's say I have a list of friend objects:
|
| 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.
| [deleted]
| rak1507 wrote:
| Some sort of function that returns a variable number of
| arguments.
|
| 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.
| piaste wrote:
| > 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.
| Spivak wrote:
| If you're doing that aren't you kinda defeating the point of
| pagination? Paginated APIs to me are a contract that says the
| size of the results can be arbitrarily large and there be
| dragons if you try to fit them in memory.
|
| With how many wrappers around paginated APIs to unpaginate
| them I must be wrong but it still bugs me.
| piaste wrote:
| Sometimes the pagination is optional to allow the client to
| only get as many results as they can handle.
|
| 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.
| nicoburns wrote:
| It occurs commonly any time you are dealing with nested data.
|
| 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)
| ReleaseCandidat wrote:
| When `x => [x * 2]` is a function that returns an array, but
| you do not want an array of arrays. Doesn't make much sense
| with such a lambda, of course.
|
| And of course you could call `flatmap` bind, if you prefer ;)
| saranshk wrote:
| added this to the post as well, a place where there is an array
| inside an array of objects and you want all those. For example,
| if we want to extract all roles from the array:
|
| [ { name: "Saransh Kataria" , roles: ['system-admin',
| 'developer'] }, { name: "Wisdom Geek" , roles: ['basic'] },
| ].flatMap(x => x.roles);
|
| // Output => ["admin", "system-admin", "developer"]
| khalilch2 wrote:
| wouldn't the output be ['basic', 'system-admin', 'developer']
| rather than array[0] being 'admin'?
| tzs wrote:
| You are right that it should be 'basic', not 'admin'. But
| the order should be 'system-admin', 'developer', and
| finally 'basic'.
| saranshk wrote:
| you are right, I was trying to format it for HN but somehow
| screwed up copy pasting, though it'd be ["system-admin",
| "developer", "basic"] and not ['basic', 'system-admin',
| 'developer']
| [deleted]
| krylon wrote:
| This stirs up some memories of Perl, where flattening
| arrays/lists is the default, and you need explicit references to
| have nested arrays or hash tables. I remember it used to be a
| kind of foot-seeking gun for newbies, but I never ran into
| trouble with it once I understood references (which isn't super
| hard).
___________________________________________________________________
(page generated 2022-01-05 23:02 UTC)