skip to content

How can a caller pin a whole nested parameterized shape, such as a list of pairs, when a flat token cannot name one?

level: seniorimportance: nice to knowfreq 28%

answer

  1. one name cannot hold a nesting
  2. declarations keep what values lose
  3. put the shape in a supertype argument
  4. an empty subtype pins it
  5. loader reads the shape off the instance

basics

~20 s

Put the shape in a declaration instead of a value: declare an empty subtype whose supertype argument is the whole nested shape, hand an instance of it over, and let the loader read the shape off that supertype.

solid answer

~50 s

A flat token stands for one type and has nowhere to put arguments, so it can say *list* but not *list of pairs of text and number*. The trick is to move the shape to where a platform keeps it: a **declaration**. The caller declares an empty subtype of a capture type parameterized by the whole shape, and passes an instance of that subtype as the evidence. The nested arguments are now fixed in the subtype's own declaration rather than in a value, so they survive compilation as declaration metadata and the loader can read them back and drive a nested conversion — outer container first, then each element. The costs are real: a subtype and an object per distinct shape, evidence that is awkward to build dynamically, and a technique that only works where a platform retains a declaration's supertype arguments at all.

code

pseudocode · 8 lines
pseudocode
# a flat token names a container but not what it holds
abstract type ShapeOf<S>            # empty: it exists to carry S

# the argument is written in a DECLARATION, so it survives compilation
endpointsShape = new ShapeOf<List<Pair<Text, WholeNumber>>> { }

endpoints = loader.read("api.endpoints", endpointsShape)
# loader walks the shape: build a list, then convert each pair's two parts

go deeper

for a junior

The takeaway is the limit, not the trick: a single type name has no room for the types inside it, so a list of pairs needs something richer than one name handed over.

for a middle

Explain the move from values to declarations: arguments attached to a value are gone, arguments written into a declaration are part of it, so an empty subtype of a parameterized capture type becomes the evidence.

for a senior

Show the whole walk — read the shape, convert the container, convert and check each element — and name the costs you accepted: a declaration per shape, no run-time assembly, and a platform that must retain the metadata.

for a principal

The call to own is whether a general shape-driven reader belongs in the codebase at all. A handful of purpose-built typed reads is often cheaper than an idiom every reader must learn, and the general form only pays once the shapes are many and unpredictable.

## Why one name is not enough A class token stands for a type by naming it. That works for a port, a flag or a duration. It stops working the moment the wanted type is itself parameterized: a settings entry holding a list of endpoint pairs needs the loader to know the container **and** what the container holds, because the conversion is recursive — split the text into elements, convert each element, collect them back. A flat token has no slot for that. It names the container and nothing else, so the loader gets as far as "build a list" and then has to guess what to put in it. Guessing from the text is not typing, and taking one token per level does not help either: three tokens in a row do not say how the levels compose, and two nestings with the same set of parts differ only in their arrangement. ## Putting the shape where declarations live The insight is about **where** a type argument survives. An argument attached to a value is discarded — that is the premise of this whole subject. An argument written into a **declaration** is part of that declaration, and platforms that keep signature metadata keep it. So the caller manufactures a declaration whose only purpose is to hold the shape: 1. a capture type is declared, parameterized by one placeholder and carrying no behaviour at all; 2. the caller declares an **empty subtype** of that capture type, with the whole nested shape written as the supertype's argument; 3. the caller passes an instance of that subtype as the evidence; 4. the loader reads the shape back off the instance's supertype and walks it — container, then element, then the element's own parts — selecting a conversion at each level. The instance itself carries nothing; it is a handle on the declaration. That is the part candidates usually get backwards: constructing the object does not record the shape, the *declaration* did, and the object exists only so the caller has something to pass. ## What it costs | | Flat token | Captured shape via an empty subtype | |---|---|---| | What it can express | One type, no arguments | A whole nesting, to any depth | | Where the information lives | In a value naming a type | In a declaration's supertype argument | | Built at run time from parts? | Easy | Awkward — a declaration is not data | | Cost per distinct shape | None | One subtype, and usually one instance | | Availability | Wherever tokens exist | Only where declaration metadata is retained | The last row matters and is worth stating carefully: platforms differ in what they keep. Some retain a declaration's supertype arguments as metadata precisely so tools can read them; some keep type arguments generally and need no trick at all; some keep nothing, and there the idiom simply does not exist. Do not present it as a universal mechanism. ## Practical consequences - **Hoist the evidence.** One captured shape per distinct type, declared once and reused, rather than a fresh subtype at every call — otherwise the technique quietly multiplies declarations. - **Keep the capture type empty.** The moment it carries behaviour, callers subclass it for reasons other than evidence, and the shape stops being the point. - **Check at the leaves.** The nested conversion should narrow each converted element, not just the container: a list that is the right container with a wrong element type is the failure this idiom exists to catch. - **Do not accept a shape built from parts.** If the shape can be assembled at run time from data, it is data, and nothing guarantees it describes anything the caller declared. - **Reach for it last.** Where the nesting is fixed and small, a purpose-built read — one that knows it returns a list of endpoint pairs — is simpler and needs no evidence at all. ## Saying it well in an interview The answer has three moves, and stating them in order is what shows understanding: a flat name has nowhere to put arguments; arguments fixed in a declaration survive where arguments attached to values do not; therefore manufacture a declaration — an empty subtype of a parameterized capture type — and pass a handle on it. Then volunteer the limits, because the limits are what separate someone who has used the idiom from someone who has read about it.

  • Why not just pass one token per level — a token for the list, one for the pair, one for each part?
    Because a list of tokens records parts, not arrangement. The same parts describe several different nestings, and nothing in the list says which one was meant or how deep each level goes. The captured supertype keeps the arrangement because the shape was written as a single type expression.
  • What goes wrong if the nested conversion checks only the outer container and not the elements?
    You get the failure the idiom was meant to prevent: the value is the right container, so the read succeeds, but an element is the wrong type. It circulates until something touches that element, and the report names a use far away from the store rather than the key that was misconfigured.

saying these in an interview costs you the question

  • Thinks a flat token can carry a nested shape's arguments
  • Says constructing the instance is what records the shape
  • Expects the loader to infer the nesting from stored text
  • Assumes the idiom works on every platform regardless of metadata
  • Declares a new capture subtype at every call site