skip to content

A teammate rewrites two consecutive mapping stages over a wrapped value as one — which law makes that safe, and when does it break?

level: seniorimportance: nice to knowfreq 32%

answer

  1. two stages or one, same answer
  2. mapping the identity changes nothing
  3. laws license a rewrite, not a proof
  4. a counter inside map is observable
  5. composition law first, identity law second

basics

~20 s

The functor composition law: mapping one function and then another must equal mapping the two as a single function in one pass. The rewrite is safe only while mapping touches nothing but the contained value — a mapping that also counts, logs or re-applies breaks it.

solid answer

~40 s

A well-behaved mapping rung obeys two laws. **Identity**: mapping the function that returns its argument unchanged must give back a context indistinguishable from the original. **Composition**: mapping `f` and then mapping `g` must equal mapping the single function that applies `f` then `g`. The second law is what licenses the rewrite — fusing two stages into one is guaranteed not to change the result. It breaks the moment mapping does anything beyond transforming the contained value: counting how many stages ran, recording each stage somewhere observable, applying the function twice, or normalising the value on the way through. None of that is caught by a compiler, so a hand-written context that violates the laws silently invalidates a refactoring every reader assumes is safe.

code

pseudocode · 12 lines
pseudocode
// two stages
a = map(map(setting, trim), parsePort)

// one stage, licensed by the composition law
b = map(setting, value -> parsePort(trim(value)))
// a and b must be indistinguishable for every setting

// a mapping that breaks the law: each stage is observable
function map(wrapper, f):
    wrapper.stagesRun = wrapper.stagesRun + 1
    return wrap(f(wrapper.value))
// the two-stage form now reports 2, the fused form reports 1

go deeper

for a junior

Recall the promise rather than the wording: mapping twice must give the same answer as mapping once with the two steps joined, and a no-op stage must change nothing.

for a middle

Explain both laws as statements about what an observer can distinguish, and give one concrete implementation choice — a counter, a log, a double application — that breaks each.

for a senior

Show how you would test a hand-written context against the laws, including the empty and failed cases, and explain what class of refactoring goes wrong once a law is violated.

for a principal

Decide whether shared context types in your codebase carry law tests as a release requirement, and weigh that against the cost of teams writing their own one-off wrappers.

## What a law is, at this altitude A law here is not a proof obligation for a theorem — it is a promise that makes a **local rewrite safe without reading the implementation**. Each law says: two differently written expressions must be indistinguishable to any observer. That is precisely what a refactoring needs. When the law holds, you can perform the rewrite anywhere in a codebase you have never read; when it does not, every such rewrite is a potential behaviour change. ## The two functor laws - **Identity.** Mapping the function that returns its argument unchanged must produce a context indistinguishable from the original — same contents, same emptiness or failure, same everything an observer can see. - **Composition.** Mapping `f` and then mapping `g` must equal mapping the single function that applies `f` and then `g`. Both are statements about *observability*. A mapping implementation may do whatever it likes internally as long as no caller can tell the two forms apart. The moment a caller can, the law is broken. ## The refactorings they license | law | rewrite it makes safe | |---|---| | identity for mapping | deleting a stage whose function returns its input unchanged | | composition for mapping | fusing two consecutive mapping stages into one | | left identity for chaining | replacing a chain over a freshly wrapped value with a direct call to the step | | right identity for chaining | deleting a trailing step that only re-wraps the value it was handed | | associativity for chaining | extracting a run of consecutive chained steps into one named helper | The associativity entry is the one engineers use most without noticing. It says that running the first step and then a helper that runs the rest gives the same result as running the first two and then the last. That regrouping is exactly what happens when a long configuration chain is broken into named pieces for readability — and it is safe only because the law holds. The rungs above mapping add laws of the same flavour. The applicative rung requires that lifting a value which needs no context and then combining agrees with simply applying the function to the values, and that regrouping which contexts are combined first does not change the result. ## How a hand-written context breaks them Breaking a law rarely looks like a mistake. It looks like a feature: - **A counter or a log inside the mapping operation.** Now two stages report two events and one fused stage reports one. The forms are distinguishable, so composition is broken — and the identity law goes with it, because an apparent no-op stage now changes what an observer sees. - **A mapping that applies the function twice**, perhaps to recompute a cache. Composition fails for any function that is not idempotent. - **A mapping that replaces an empty context with a filled one**, or normalises the value on the way through. Identity fails immediately: the no-op stage is no longer a no-op. - **A mapping that reorders or deduplicates** the context's contents. Fusing two stages may then produce a different arrangement from running them separately. None of these are reported by a type checker. The type of a law-violating mapping operation is identical to the type of a law-abiding one, which is why the laws are a design obligation on whoever writes the context rather than something the surrounding language enforces. ## Checking the laws in practice The laws are unusually easy to test, because each is an equality between two expressions over the same input: 1. Generate values of the context, including the empty or failed cases, not only filled ones. 2. For identity, assert that mapping the unchanged-argument function equals the original. 3. For composition, assert that mapping `f` then `g` equals mapping the composed function, for a pair of functions that are deliberately not commutative so an accidental swap is caught. The empty and failed cases matter most: implementations usually get the filled case right and break on the one they thought was uninteresting. ## Why an interviewer asks Not to hear the laws recited. The question separates a candidate who treats the laws as decoration from one who can name the consequence: **a law is what makes a refactoring checkable locally**. If someone in your codebase writes a wrapper whose mapping operation is not law-abiding, nothing fails loudly. Instead, a class of rewrites that everyone believes to be meaning-preserving — fuse two stages, delete a no-op, extract a helper from a chain — quietly stops being meaning-preserving, and the resulting bug surfaces a long way from the wrapper that caused it.

  • What does the identity law for the mapping rung rule out in practice?
    It says mapping the unchanged-argument function must give back a context no observer can distinguish from the original. That rules out a mapping that also logs, counts, caches, normalises the value, or replaces an empty context with a filled one — each would make an apparent no-op stage change the program.
  • If a wrapper you wrote violates the laws, what actually goes wrong for its users?
    Nothing fails loudly. Every rewrite a reader believes is safe — fusing two stages, deleting a no-op, extracting a helper out of a chain — can silently change behaviour, and the resulting bug surfaces far from the wrapper. The laws are what let a rewrite be checked locally, without reading the implementation.

saying these in an interview costs you the question

  • Says the laws are theory with no consequence for refactoring
  • Thinks a mapping that logs each stage is still law-abiding
  • Believes a compiler checks these laws for any wrapper
  • Cannot say what mapping an unchanged-argument function should return
  • Tests only the filled case and never the empty one