When a placeholder carries several requirements, what type do you write to store or pass on a value satisfying all of them?
answer
- the type nobody wrote down
- fine where requirements are declared
- a field needs a nameable type
- propagate the list onward
- widening loses what narrowing cannot prove
basics
~20 sUsually none: the combination is anonymous and exists only in the requirement list. You keep both capabilities by giving the receiving routine its own placeholder with the same requirements; storing the value in a field normally forces a named type that carries both.
solid answer
~40 sInside the routine the value has a type nobody wrote down - the combination of its requirements - and many type systems can express that only in the position that declares a placeholder. So a value is passed on by propagating the requirement list: the helper declares its own placeholder with the same requirements, and the call type-checks. Widening the value to one of its requirements compiles but discards the others from the type system's view, and reaching them again needs an unchecked narrowing. A long-lived field is the hard case, because a field needs a nameable type: either the type system offers a first-class combination you can write in any position, or you introduce a named type carrying both - which brings back the redeclaration cost a requirement list was avoiding.
code
pseudocode · 11 linesfunction buildBoard<D requires Orderable, Renderable>(departures, cutoff)
earliest = minimumOf(departures) // still D: both requirements hold
return describe(earliest, cutoff) // so the helper must demand both too
function describe<E requires Orderable, Renderable>(entry, cutoff)
if entry.departsBefore(cutoff) then
return 'DEPARTED ' + entry.toFixedWidthLine(30)
return entry.toFixedWidthLine(40)
// there is usually no type name to write for 'both at once'
// outside the position that declares a requirement listgo deeper
Recall that the helper receiving such a value has to declare the same requirements itself; passing it into a routine that demands less silently drops capabilities.
Explain why propagation type-checks with no cast: the helper's own placeholder is inferred at the call site and carries both requirements by declaration.
Diagnose the widen-then-narrow chain in real code: name the signature that dropped the information and fix it there rather than adding a narrowing at the point of use.
Decide when the repeated list has earned a named carrier type, and own the migration for every candidate type that must then be declared under it.
## The value has a type nobody wrote Inside a routine whose placeholder is required to be both orderable and renderable, a departure value is simultaneously usable as each. Its type is the **combination** of the two requirements: the set of types satisfying both, carrying the union of their operations. No declaration in the program introduces that type, and in many type systems no syntax exists to write it anywhere except the position that declares the placeholder's requirements. This is what makes the subject a real interview question rather than a syntax note. The combination is easy to demand and awkward to keep: every time the value leaves the routine that demanded it, the type system needs somewhere to record that both requirements still hold. ## Three ways to keep both requirements | Way | What you write | What it costs | |---|---|---| | Propagate the placeholder | the helper declares its own placeholder with the same requirement list | the list is repeated at every signature that handles the value | | Write the combination directly | a type position naming both, where the type system allows it | only available where first-class combined types exist; long to read | | Introduce a named type | one declared type carrying both capabilities | every candidate type must be declared under it, which you may not control | Propagation is the default answer and the one an interviewer is listening for. The helper is generic too; its placeholder is inferred from the argument at the call site; both requirements travel because the helper demanded both. Nothing is cast, nothing is checked again. Some type systems do provide a combination you can write in any type position - a field, a variable, a return type. There the problem dissolves, and what remains is readability: the full list appears at every position, and diagnostics print the whole thing. ## What widening costs The tempting shortcut is to store the value under one of its requirements - the ordering contract, say - and carry on. The value itself does not change: at run time it still has every operation it always had. What changes is what the type system knows. Once the variable is typed by one requirement, the other requirement's operations are no longer reachable, and getting them back means an **unchecked narrowing** the compiler cannot verify. That converts a guarantee the caller already established into a possible failure at the point of use, and it does so silently, far from the call site that had the information. The same applies to widening all the way to a universal root type and testing at run time: it compiles, it is slower, and it throws away a fact the signature had already proved. ## At the compiled boundary Where a type system discards type arguments after checking them, the placeholder is compiled to one representative - the first entry in the requirement list - and conversions are inserted where members of the other entries are reached. That is invisible to the source, and it does not weaken the guarantee: the check already happened at the call site, and the inserted conversions are known to succeed. It does mean that a value of a combined type has no single run-time name to inspect, and that the combination is not something a run-time test can ask about as a whole. Where a type system keeps type arguments available instead, more of the structure survives, but the source-level naming problem is the same. ## Practical rules 1. Reach for propagation first: give the receiving routine the same requirement list and pass the value straight through. 2. If the list is repeating across more than a handful of signatures and you own the candidate types, that is the evidence for introducing a named type carrying both - not aesthetics. 3. Never widen and narrow back inside the same call chain; if you wrote a narrowing that you know cannot fail, the signature above it lost information it should have kept. 4. For a long-lived field, decide deliberately. A field needs a nameable type, so the options are a first-class combined type where the language has one, a named carrier type, or two fields holding the value under each requirement - which is a duplication that can drift. 5. Keep the list short. Each extra requirement is repeated at every signature the value passes through, so the naming problem grows with the list, not with the number of call sites.
- What is lost if you store the value under just one of its two requirements?Every operation declared by the other. The value still has them at run time, but the type system no longer records the fact, so reaching them again needs an unchecked narrowing whose success the compiler cannot prove even though the caller had already established it.
- In a type system that lets you write the combination in any type position, what changes?The propagation problem disappears: a field, a variable or a return type can name it directly and no carrier type has to be invented. What remains is readability - the whole list appears at every position, and diagnostics print all of it.
saying these in an interview costs you the question
- Assumes the combination can be written wherever a named type can
- Widens to one requirement and claims nothing was lost
- Thinks a cast at the call site restores a requirement the variable's type dropped
- Believes the combination is re-checked when the value is passed on
- Reaches for a named carrier type before trying to propagate the list