skip to content

What do you learn about a fold by rewriting a mapping stage and a filtering stage as folds?

level: middleimportance: nice to knowfreq 32%

answer

  1. one operation underneath the other two
  2. seed it with an empty collection
  3. the step decides what each element contributes
  4. the accumulator's type is free
  5. general, but not the most readable

basics

~20 s

That a fold is the general one. Seed it with an empty collection and both stages fall out of it, which shows the accumulator's type is free and a fold can rebuild a structure as easily as it can produce a number.

solid answer

~50 s

Seed a fold with an empty collection and the per-element stages appear as special cases. A mapping stage is a fold whose step appends the converted element: `combine(acc, t) = append(acc, convert(t))`. A filtering stage is a fold whose step appends conditionally: `combine(acc, t) = if keep(t) then append(acc, t) else acc`. What that shows is that the accumulator type is genuinely free - the result of a fold can be a number, a record or a whole collection - so fold is the general collapsing operation and the shape-preserving stages are constrained versions of it. The lesson is not to write everything as a fold. A named mapping or filtering stage tells a reader what happens to each element at a glance, while a fold only says "something is accumulated"; the general operation is the one to reach for when the result genuinely changes shape.

code

pseudocode · 10 lines
pseudocode
// filtering, expressed as a fold over an empty collection
cardOnly = fold(transactions, [],
    function(acc, t) {
        if isCard(t) then return append(acc, t)
        else return acc
    })

// mapping, expressed the same way
netAmounts = fold(transactions, [],
    function(acc, t) { return append(acc, netOf(t)) })

go deeper

for a junior

Know that a fold's result does not have to be a number - seeded with an empty collection, its accumulator is a collection like any other.

for a middle

Write the two rewrites out, name what each special case gives up, and say why the named stages are still the better code where they fit.

for a senior

Call out the obligations the rewrite inherits: the appending cost when extending the accumulator copies it, and the ordering the original stage was quietly guaranteeing.

for a principal

Set the readability rule for the team - reach for the most specific operation that does the job, and reserve the general one for results that genuinely change shape.

## The two rewrites Take the day's transactions. A stage that converts each one to its net amount, and a stage that keeps only the card payments, both become folds over an empty collection: ```pseudocode // mapping stage as a fold netAmounts = fold(transactions, [], function(acc, t) { return append(acc, netOf(t)) }) // filtering stage as a fold cardOnly = fold(transactions, [], function(acc, t) { return isCard(t) ? append(acc, t) : acc }) ``` Neither rewrite needed a new construct. The seed chose the accumulator's type, and the step decided what - if anything - each element contributed to it. ## What the rewrite actually demonstrates - **The accumulator's type is free.** This is the headline. A fold is not "the thing that produces a number"; it produces whatever the seed's type is, and a collection is an ordinary choice of seed type. - **The shape-preserving stages are constrained folds.** A mapping stage is the special case where every element contributes exactly one entry and the converted value ignores the accumulation so far. A filtering stage is the case where every element contributes one entry or none, unchanged. Both give up the freedom to look at what has accumulated. - **Fold is the general consumer of the structure.** It is the one operation in this vocabulary that can change the result's shape, which is why folding is sometimes given the general name **catamorphism**: the operation that collapses a structure according to how the structure is built. - **Going the other way does not work.** A mapping stage and a filtering stage each hand back a structure of the same kind and never expose a running accumulation, so there is no arrangement of them that turns a thousand transactions into one totals record. Generality runs one way here. | Operation | Result shape | Entries out, per element in | Can the step see the accumulation? | |---|---|---|---| | Mapping stage | same kind of structure | exactly one | no | | Filtering stage | same kind of structure | one or none, unchanged | no | | Fold | whatever the seed's type is | not applicable - one result overall | yes | ## Why this is a lesson and not a licence Every interview answer here has a second half, and leaving it out is what makes a candidate sound clever rather than employable. **Do not rewrite readable stages as folds.** A named per-element stage announces its contract in its name: same count out as in, or a subset in the original order. A fold announces nothing except that an accumulator exists, and the reader must decode the step to find out whether the count changed, whether order survived, and what type comes back. The working rule is to reach for the **most specific operation that does the job**, and to reach for the fold when the result genuinely changes shape - one record, one total, one largest sale, one grouping - or when the step needs to look at what has accumulated so far, which is the one thing the shape-preserving stages structurally cannot do. ## The cost trap in the rewrite The rewrites above append to the accumulator once per element, and appending is where the hidden cost sits. If each append copies the collection accumulated so far, an n-element fold does work proportional to n squared, and the rewrite that was supposed to be equivalent is quadratic where the original was linear. The usual fixes are to accumulate into a structure whose extension shares its existing contents rather than copying them, or to accumulate into a mutable local builder that never escapes the fold and to hand back a finished value at the end. State what `n` is when you discuss this: it is the number of elements, and the copying cost is per element, which is where the second factor comes from. ## A related sharp edge If the accumulator is a collection and the step prepends rather than appends, the result comes out reversed. That is a real defect in a reconciliation report where the order is the day's sequence, and it is a favourite follow-up: the interviewer wants to see that you noticed the ordering obligation the original stage was quietly meeting for you. ## What an interviewer is checking This is a differentiator question rather than a screening one. It separates a candidate who has memorised three named operations from one who sees the vocabulary's structure - and the good answer ends by declining to use the general operation where a specific one reads better.

  • If both stages are folds, why keep them as separate named operations?
    Because the name carries the contract. A mapping stage promises one result per element and a filtering stage promises a subset in the original order, and a reader gets both promises without reading the step. A fold promises only that something accumulated, so the reader must decode it.
  • Can a mapping stage and a filtering stage be combined to express a fold?
    No. Both hand back a structure of the same kind and neither exposes the running accumulation to the step, so nothing in that pair collapses a structure to a single value of a different type. The generality runs one way only.
  • What is the cost trap when the accumulator is a collection?
    Appending once per element is only cheap if extending the accumulator does not copy what is already there. If it does copy, an n-element fold does work proportional to n squared. Accumulate into a structure that shares its contents, or into a local builder that never escapes the fold.

saying these in an interview costs you the question

  • Says a fold can only ever produce a single number
  • Rewrites readable per-element stages as folds to show off
  • Claims those stages can express a fold in return
  • Insists the accumulator must have the element's type
  • Ignores the copying cost of appending into the accumulator
  • Prepends into the accumulator and does not notice the reversal