skip to content

In a composed text-normalisation pipeline, when is a stage that returns its input unchanged genuinely useful?

level: juniorimportance: should knowfreq 50%

answer

  1. hands back what it was given
  2. a stage that changes nothing
  3. neutral element of composition
  4. the off position of an optional stage
  5. starting value when combining stages

basics

~20 s

A stage returning its input unchanged is the identity function: composing it changes nothing. It earns its place as a neutral default - what an optional stage falls back to, and the starting point when a list of stages is combined into one.

solid answer

~50 s

The identity function satisfies `identity(x) = x` for every input, and it is the neutral element of composition: writing `compose(f, g)` for the stage `x -> f(g(x))`, both `compose(f, identity)` and `compose(identity, f)` behave exactly like `f`. That is precisely why it is useful rather than dead code. When a normalisation stage is switched off by configuration, the slot still has to hold a stage of the right shape, and identity is the honest one to put there. When a configured list of stages is collapsed into a single pipeline, identity is the starting value, so an empty list still yields a usable pipeline instead of an error. It is also the baseline in tests, letting you isolate one stage at a time. What it is not is a pass-through that also logs or records - that has an observable effect, so it is not identity.

code

pseudocode · 18 lines
pseudocode
function identity(x)
    return x

// compose(f, g) means the stage  x -> f(g(x))

function buildNormaliser(config)
    caseStage = identity
    if config.foldCase then
        caseStage = toLowerCase

    accentStage = identity
    if config.stripAccents then
        accentStage = removeAccents

    return compose(accentStage, compose(caseStage, trim))

// with both switches off, the returned stage behaves exactly like trim
// with both switches on, it trims, lowercases, then strips accents

go deeper

for a junior

Be able to state it in one line: the identity function returns its argument unchanged, so putting it in a chain changes nothing. Then name one use, such as the value a switched-off optional stage falls back to.

for a middle

Explain the neutral-element law on both sides, and why that makes identity the right starting value when a configured list of stages is collapsed into a single pipeline function.

for a senior

Show where it removes branching in real assembly code: a slot that must always hold a stage, a default supplied to a caller, a stage swapped out to bisect which step corrupted an index entry.

for a principal

The trade-off you own is uniformity against call overhead: a branch-free pipeline shape is easier to configure and test, and the law says identity stages can be stripped at assembly time if profiling says the calls matter.

## What the identity stage is A pipeline stage here is a function from a value to a value of the same shape - text in, text out. The **identity function** is the one stage that hands back its argument untouched: `identity(x) = x` for every `x`, with no copying, no trimming, and nothing written anywhere. It is **total** (defined on every input), **pure** (nothing observable changes), and its output type is its input type, which is exactly what lets it sit at any position in a chain of same-typed stages. Its interest is not what it computes - nothing - but what it does to composition. Write `compose(f, g)` for the stage `x -> f(g(x))`. Then: - `compose(f, identity)` behaves exactly like `f`, because identity hands `f` the original input. - `compose(identity, f)` behaves exactly like `f`, because identity passes `f`'s result straight through. Both directions hold, and that two-sided property is what **neutral element** means. Identity is to composition what zero is to addition and what the empty list is to concatenation: you may insert it or delete it anywhere in a chain without changing the value the chain produces. ## Where a no-op earns its place A reviewer who sees a stage that does nothing and reaches for the delete key is usually wrong. Four uses are real: 1. **The off position of an optional stage.** A normaliser configured with accent-stripping disabled still needs something in that slot, because the slot's type is *a stage*. Identity fills it without a conditional branch inside the pipeline, so the pipeline's shape stays the same whatever the configuration says. 2. **The starting point when a list of stages is collapsed into one.** Combining a configured list into a single stage needs a value to start from, and identity is the only one that leaves a one-element list alone. It also gives the empty list an answer: a pipeline that does nothing, rather than a crash. 3. **A default for a slot that must hold a stage.** A caller who supplies no key-extractor or no pre-processor gets identity, which keeps the calling code branch-free. 4. **A baseline in tests and in bisection.** Replacing one stage with identity is how you find which stage changed an index entry, without rebuilding the pipeline by hand. ## What identity is not - **Not a copy.** A stage that returns a fresh but equal value may be fine in a pure setting, but it is a different function, and on a type with reference semantics the difference is observable. - **Not a pass-through with an effect.** A stage that returns its input *and* writes a log line is not identity: composing it changes something observable, so the neutral-element law no longer applies to it. - **Not a validator.** A stage that returns its input when the input is acceptable and raises otherwise is not total, so it is not identity either. - **Not tied to a position.** Because the law holds on both sides, identity is equally harmless first, last or in the middle. ## The laws, checked in both directions | Expression | Behaves like | Why | |---|---|---| | `compose(f, identity)` | `f` | identity gives `f` the untouched input | | `compose(identity, f)` | `f` | identity passes `f`'s output through | | `compose(identity, identity)` | `identity` | neither stage changes anything | | combining an empty stage list | `identity` | nothing was applied, so nothing changed | | `compose(f, logAndReturn)` | not `f` | the write is observable, so it is not a neutral stage | ## Cost, and when to take it out The *semantic* cost of an identity stage is exactly zero; the *run-time* cost is one call through a function value, which in a hot loop over millions of documents is measurable but rarely dominant. Two honest positions: leave it in, because the uniform shape is what makes the pipeline configurable; or strip it when the pipeline is assembled, since the law guarantees that dropping identity stages from a chain cannot change the result. Language runtimes differ in how much of that they do for you - some collapse a chain of small calls aggressively, others do not - so measure before hand-optimising. ## What an interviewer is listening for The weak answer is 'a function that does nothing, so it is useless'. The answer that lands states the law in both directions, then names one place it pays: the off switch, or the starting value that makes an empty configuration produce a working pipeline instead of an error. Mentioning that a logging pass-through is *not* identity shows you understand the law is about observable behaviour, not about the returned value alone.

  • A stage returns its input unchanged but also appends a line to an audit log. Is it identity?
    No. Identity changes nothing observable, and the write is observable. The returned value matching the argument is only half the requirement. Practically it matters because you can drop identity stages from a chain freely, whereas dropping the logging stage removes audit records - the composition law does not cover it.
  • What should combining an empty list of configured stages produce?
    The identity stage. That is the neutral element, so it is the only answer consistent with combining a one-element list yielding that single stage. It also means an empty configuration produces a working pipeline that passes text through untouched, rather than a null stage or an error the caller has to special-case.

saying these in an interview costs you the question

  • Calls every no-op stage dead code that should be deleted
  • Says an identity stage returns a copy rather than the same value
  • Claims identity is only neutral at the start of a pipeline
  • Treats a pass-through that also logs as the identity function
  • Thinks an empty stage list should raise instead of yielding identity