skip to content

Mapping & Reducing

map, flat_map, inject and sum reshape or collapse a collection, and group_by, tally, zip and chunk_while regroup it. Interviewers probe reduce's initial value, float sums and when tally fits.

on this pageshow

explore

questions

6

In Ruby, how do map and flat_map differ when each franchise region returns an array of store totals?

level: juniorimportance: must knowfreq 60%

answer

  1. same size vs concatenated
  2. one level only
  3. non-array results kept as is
  4. map.flatten goes all the way down
  5. aliases collect and collect_concat

basics

~20 s

map 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 lines
ruby
Region = 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

for a junior

Recall that map returns one result per element and flat_map joins array results into one flat list.

for a middle

Explain that flat_map removes exactly one level, passes non-array results through, and differs from map.flatten on nested data.

for a senior

Replace map.flatten chains that silently destroy pair structure, and use flat_map for zero-or-many expansions in review.

for a principal

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
open as a page

In Ruby, how do inject's symbol, block and initial-value forms work, and why does `sales.inject { |sum, s| sum + s.amount }` fail?

level: middleimportance: must knowfreq 58%

basics

~20 s

Without an initial value, inject uses the first element as the memo, so the first call computes Sale + Integer and raises NoMethodError. inject(0) { ... } fixes it; inject(:+) and inject(0, :+) name a method instead of a block.

open as a page

In Ruby, what do min, max and minmax return on an empty list, with a count argument, and with a one-parameter block?

level: middleimportance: should knowfreq 36%

basics

~20 s

On an empty list min and max return nil and minmax returns [nil, nil]. With a count, min(n) and max(n) return arrays, largest first for max. A block must compare two elements like <=>; a one-parameter block silently returns the wrong element.

open as a page

In Ruby, when do you use tally, group_by or to_h with a block to turn a list of franchise sales into a hash?

level: middleimportance: should knowfreq 50%

basics

~20 s

tally counts equal elements into element => count; group_by collects elements under the block's key into key => array; to_h with a block builds one key-value pair per element, where a repeated key silently keeps the last value.

open as a page

In Ruby, a franchise report totals Float sale amounts with inject(:+) in one service and sum in another, and the totals differ in the last digits; why, and what do you change?

level: seniorimportance: should knowfreq 35%

basics

~20 s

sum adds Floats with Kahan-Babuska compensated summation, which tracks and corrects rounding error; inject(:+) adds naively, so errors accumulate. Use sum everywhere for consistency, and store money as Integer cents or Rational so no Float rounding enters the total.

open as a page

In Ruby, how would you pair each month's franchise sales with the same month last year and group consecutive months of growth?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

this_year.zip(last_year) pairs values by position, keeping the receiver's length and padding a shorter argument with nil. chunk_while then groups adjacent elements while its two-element block stays true; slice_when is the same but splits where its block is true.

open as a page