A teammate rewrites two consecutive mapping stages over a wrapped value as one — which law makes that safe, and when does it break?
answer
- two stages or one, same answer
- mapping the identity changes nothing
- laws license a rewrite, not a proof
- a counter inside map is observable
- composition law first, identity law second
basics
~20 sThe 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 sA 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// 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 1go deeper
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.
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.
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.
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