In Ruby, how do map and flat_map differ when each franchise region returns an array of store totals?
answer
- same size vs concatenated
- one level only
- non-array results kept as is
- map.flatten goes all the way down
- aliases collect and collect_concat
basics
~20 smap returns a new array with one block result per element, so array results stay nested. flat_map concatenates array results one level into a single flat array, keeps non-array results unchanged, and never flattens deeper, unlike map followed by flatten.
solid answer
~40 s`map` (alias `collect`) always returns a new array the **same size** as the receiver: `regions.map { |r| r.store_totals }` gives an array of arrays, one per region. `flat_map` (alias `collect_concat`) **concatenates** each block result that is an array into one flat array, so `regions.flat_map(&:store_totals)` gives every store total in one list. It flattens exactly **one level**: nested arrays inside the block's result survive, and a block result that is not an array is appended as it is. `map { ... }.flatten` is not equivalent: plain `flatten` recurses through every level and builds an extra intermediate array; `flatten(1)` matches `flat_map`. Both methods return an `Array` even on a `Hash`.
code
ruby · 16 linesRegion = Data.define(:name, :store_totals)
regions = [
Region.new(name: "north", store_totals: [1200, 800]),
Region.new(name: "south", store_totals: [450]),
Region.new(name: "west", store_totals: [])
]
regions.map(&:store_totals) # => [[1200, 800], [450], []]
regions.flat_map(&:store_totals) # => [1200, 800, 450]
regions.flat_map { |r| [[r.name, r.store_totals.sum]] }
# => [["north", 2000], ["south", 450], ["west", 0]]
regions.map { |r| [[r.name, r.store_totals.sum]] }.flatten
# => ["north", 2000, "south", 450, "west", 0]
[1, 2].flat_map { |x| x * 2 } # => [2, 4]go deeper
Recall that map returns one result per element and flat_map joins array results into one flat list.
Explain that flat_map removes exactly one level, passes non-array results through, and differs from map.flatten on nested data.
Replace map.flatten chains that silently destroy pair structure, and use flat_map for zero-or-many expansions in review.
Encourage transformations whose output shape is obvious from the method name, so data contracts between steps stay readable.
## `map`: one result per element `map`, also called `collect`, calls the block once per element and returns a **new array** of the block's results, in the same order and with the **same size** as the receiver. It never changes the receiver; `map!` on arrays is the in-place variant. For a franchise report, where each region object knows the totals of its stores: ```ruby regions.map { |r| r.store_totals } # => [[1200, 800], [450], [900, 300, 150]] ``` The result has three elements, because there are three regions. Each element is itself an array, because that is what the block returned. ## `flat_map`: concatenate the results `flat_map`, also called `collect_concat`, calls the block the same way, but instead of storing each result as one element it **concatenates** results that are arrays: ```ruby regions.flat_map { |r| r.store_totals } # => [1200, 800, 450, 900, 300, 150] ``` The rules are precise, and interviewers probe them: 1. If the block returns an **array** (or an object that converts with `to_ary`), its elements are appended to the result. 2. If the block returns anything else, that value is appended **as a single element**. `[1, 2].flat_map { |x| x * 2 }` is `[2, 4]`, not an error. 3. Only **one level** is removed. `[[1, 2], [3, [4]]].flat_map { |x| x }` is `[1, 2, 3, [4]]`; the inner `[4]` survives. ## Why not `map` then `flatten`? | Expression | Levels flattened | Intermediate array | Result for `[[1, [2]], [3]]` with identity block | |---|---|---|---| | `map { }` | none | no | `[[1, [2]], [3]]` | | `flat_map { }` | one | no | `[1, [2], 3]` | | `map { }.flatten(1)` | one | yes | `[1, [2], 3]` | | `map { }.flatten` | all | yes | `[1, 2, 3]` | Two differences matter: - **Depth.** Plain `flatten` recurses through every level. If the data legitimately contains arrays, such as `[region, total]` pairs, `flatten` destroys that structure while `flat_map` keeps it. - **Work.** `map.flatten` first builds the nested array, then builds a flat copy. `flat_map` builds the flat result directly. ## On a `Hash` Both methods come from `Enumerable`, so on a `Hash` they yield key and value and return an **Array**, never a Hash: - `{north: 300}.map { |region, total| total }` returns `[300]`. - `{north: [300, 50]}.flat_map { |region, totals| totals }` returns `[300, 50]`. Turning the result back into a hash needs `to_h` or a Hash method that keeps the type. ## Choosing in practice - One output per input: `map`. - Zero, one or many outputs per input, wanted as one list: `flat_map`, with the block returning an array (possibly empty). - A deeply nested structure that really should be flattened completely: `flatten`, applied deliberately. A useful habit is to read the block's return type before choosing: if it returns an array and you want the elements, not the arrays, you want `flat_map`. ## Zero, one or many per element `flat_map` is the natural way to express an expansion where each input yields a variable number of outputs: - Return `[]` to contribute **nothing**, for example a region with no stores that reported. - Return `[x]` or a bare non-array `x` to contribute **one** value. - Return `[x, y, z]` to contribute **several**. Only arrays, and objects that convert with `to_ary`, are spliced in. A **Hash** result is not an array and has no `to_ary`, so `flat_map { |r| {r.name => r.total} }` returns an array of one-entry hashes, not a merged hash. Likewise a `Data` or `Struct` value is appended whole. Knowing exactly what gets spliced is what lets you predict the output shape without running the code. ## Interview summary Say that `map` preserves size and nesting, that `flat_map` concatenates array results one level deep and passes non-arrays through, and that `map.flatten` differs by flattening everything. The follow-up is usually the depth example with `[4]`, so have it ready.
- What does flat_map do with a region whose store list is empty?It contributes nothing: an empty array concatenates as zero elements, so the region simply disappears from the flat result. That makes `flat_map` a natural way to express zero, one or many outputs per input. With `map`, the same region would leave an empty array in the result.
- When is map followed by flatten(1) preferable to flat_map?Rarely. `flatten(1)` gives the same one-level result but builds the nested array first, so `flat_map` is both clearer and cheaper. `map.flatten(1)` is only natural when the nested array is already needed for something else before flattening.
saying these in an interview costs you the question
- flat_map flattens every level of nesting like flatten
- flat_map raises when the block returns a non-array
- map on a Hash returns a Hash
- map.flatten always gives the same result as flat_map
- map changes the receiver in place