skip to content

When you fold a collection with a binary operation in parallel instead of left-to-right, what properties must that operation have for the result to be correct, and what role does an identity element play?

level: middleimportance: must knowfreq 52%

answer

  1. parallel fold = re-bracketing, so associativity is the requirement
  2. identity handles empty chunks and seeds
  3. monoid = associative + identity
  4. commutative only needed for out-of-order combine
  5. seed applied once per chunk if not a real identity

basics

~20 s

The operation must be associative, so any bracketing of the same ordered elements gives the same answer, letting chunks combine in a tree. An identity element gives empty chunks a value to return, so partitioning is free. Commutativity is only needed if chunks may combine out of order.

solid answer

~60 s

A sequential fold applies the operation strictly left to right. A parallel reduction splits the input into chunks, folds each chunk, then combines the chunk results - which is a *re-bracketing* of the same sequence. That is safe exactly when the operation is **associative**: `(a op b) op c == a op (b op c)`. Subtraction and division are not, so parallelizing them changes the answer. The **identity** `e`, with `e op x == x op e == x`, matters for partitioning: an empty or short chunk must still produce a value, and each worker needs a seed to start from. With an identity you may split anywhere, including into empty pieces; without one you need special-casing for empty input. **Commutativity** (`a op b == b op a`) is a separate property, needed only if the framework may combine partial results in arbitrary order. String concatenation is associative but not commutative: it parallelizes fine if partials are combined positionally, and corrupts the output if they are combined as they finish.

code

text · 6 lines
text
sequential: ((((0 - 5) - 3) - 1))        = -9
parallel  : (0 - 5) op (3 - 1) = -5 - 2   = -7   // wrong

addition is associative, so:
sequential: 0+5+3+1                      = 9
parallel  : (0+5) + (3+1)                = 9     // same

go deeper

for a junior

Know that the operation must be associative - grouping must not change the answer - and that an identity value is what empty chunks return.

for a middle

Explain the re-bracketing argument, distinguish associativity from commutativity with a concrete non-commutative example, and describe why a non-identity seed is applied once per chunk.

for a senior

Add the floating-point non-associativity caveat and run-to-run variability, and show the change-of-representation technique that turns an average or a variance into an associative combine.

for a principal

Discuss designing aggregations as monoids so they compose across parallel, distributed and incremental execution, and when to pay for a deterministic reduction order versus accepting variability.

## Sequential fold versus parallel reduction A sequential fold computes ``` ((((e op x1) op x2) op x3) ... op xn) ``` One fixed bracketing, one fixed order. A parallel reduction computes something like ``` ((x1 op x2) op (x3 op x4)) op ((x5 op x6) op (x7 op x8)) ``` Same elements, same left-to-right sequence, **different bracketing**. So the question 'when is parallel reduction correct?' is exactly the question 'when does bracketing not matter?' - and that is the definition of associativity. ## Associativity: the essential requirement An operation `op` is associative when `(a op b) op c = a op (b op c)` for all a, b, c. Then every bracketing of a fixed sequence yields the same value, so the reduction may split the input anywhere, into any number of chunks of any size, and combine the partial results in a tree. Associative examples: addition of integers, multiplication, minimum, maximum, logical and/or, bitwise operations, set union, string concatenation, merging sorted runs, taking the last-by-timestamp. Not associative: subtraction (`(5-3)-1 = 1` but `5-(3-1) = 3`), division, exponentiation, 'average of the two arguments', and most 'apply this rule to a pair' functions people invent on the spot. The practical test in an interview: ask whether re-bracketing changes the answer. If yes, the operation cannot be used directly for a parallel fold - it must be restructured (see below). ## Identity: what makes partitioning free An identity `e` satisfies `e op x = x op e = x`. Together with associativity this makes the type a **monoid**, which is the exact algebraic structure a parallel reduction needs. Why it matters operationally: - **Empty chunks.** If the framework splits the input into more chunks than there are elements, some chunks are empty. With an identity they return `e` and the combine step still works; without one, empty chunks need a special 'no value' case that must be threaded through every combine. - **Seeds.** Each worker starts its local fold from `e`. - **Empty input.** `sum([]) = 0`, `product([]) = 1`, `concat([]) = ""`. Without an identity, reducing an empty collection has no answer, which is why such APIs return an optional value instead. A subtle trap: the seed must be a true identity, not merely a plausible starting value. Folding with a non-identity seed in parallel applies that seed *once per chunk*: reducing with seed 10 and addition over four chunks adds 40, not 10. Sequential folds hide the bug because there is one chunk. ## Commutativity: often confused, rarely required `a op b = b op a`. This is *not* required for a parallel reduction that preserves element order, because associativity alone permits re-bracketing while keeping the sequence intact. It becomes required when the implementation combines partial results in **completion order** - which some frameworks do for unordered sources, since it lets whichever partial finishes first be merged immediately. The distinction matters most for operations that are associative but not commutative: string or list concatenation, matrix multiplication, merging sorted runs. Combined positionally they are correct; combined out of order they silently produce scrambled output that depends on scheduling, so it passes tests and fails under load. ## The floating-point caveat Floating-point addition is *not* truly associative: `(1e16 + 1) - 1e16` differs from `1e16 + (1 - 1e16)` because each intermediate result is rounded. In practice it is treated as associative-enough, but a consequence follows: a parallel sum of floats may give a slightly different result run to run, because the chunk boundaries - and hence the bracketing - depend on the number of workers and on scheduling. If bit-for-bit reproducibility matters, you must fix the reduction tree shape, sort inputs, or use a compensated or exact summation algorithm. Interviewers like this one because it shows whether a candidate knows that 'associative' is a mathematical property, not a programming convention. ## Restructuring non-associative operations Many useful aggregations are not associative as stated but become associative after a change of representation. The canonical example is the arithmetic mean: averaging the averages of chunks is wrong when chunks differ in size. Reduce over the pair `(sum, count)` instead - that *is* associative, with identity `(0, 0)` - and divide once at the end. The general recipe: find a richer intermediate value whose combine is associative, and project to the final answer after the reduction. ## Summary of the contract For a parallel reduction you need: an associative operation; an identity for the partition and empty cases; and commutativity in addition, only if partial results may be merged in arbitrary order. Break any of these and the failure is not a crash but a wrong number that varies with worker count.

  • Why is a parallel sum of floating-point numbers not guaranteed to give the same result on every run?
    Floating-point addition rounds after each operation, so it is not exactly associative and the result depends on the bracketing. A parallel reduction's bracketing depends on how the input was chunked and on the order partial results merged, which varies with worker count and scheduling. To get reproducible results you must fix the tree shape, sort the inputs, or use compensated or exact summation.
  • A team folds a collection in parallel with the operation 'take the maximum' and a seed of the first element instead of negative infinity. What can go wrong?
    The seed is applied once per chunk rather than once overall, so a value that is not a true identity contaminates every partial result. For maximum it happens to be harmless if the seed is a real element of the data, but the same mistake with addition and seed 10 adds 10 per chunk, and with an arbitrary sentinel it can dominate the result. Use the operation's real identity and handle the empty-input case explicitly.
  • Give an operation that is associative but not commutative, and say when that distinction bites.
    String or list concatenation, matrix multiplication, and merging sorted runs are all associative but not commutative. They parallelize correctly as long as partial results are combined in positional order. The distinction bites when a framework merges partials in completion order for unordered sources, because then the output depends on scheduling and looks intermittently corrupted rather than deterministically wrong.

Adding a long column of numbers with friends: any way you group the column gives the same total, but if the operation were subtraction, each grouping would give a different answer.

saying these in an interview costs you the question

  • Claiming commutativity is what parallel reduction requires, rather than associativity.
  • Averaging the averages of chunks and expecting the overall mean.
  • Using an arbitrary seed value and not realizing it is applied once per chunk.
  • Asserting floating-point addition is associative, so parallel sums are bit-identical.
  • Saying subtraction is fine to parallelize as long as you subtract in the same order.

context