skip to content

A container picks a constructor at run time from the argument values it holds — how does that selection differ from the compiler's?

level: middleimportance: must knowfreq 55%

answer

  1. selection needs types, values are not types
  2. the compiler resolves before anything runs
  3. runtime class fits every supertype too
  4. absence matches far too many candidates
  5. lookup by full signature is exact

basics

~20 s

Run-time selection sees only the runtime classes of the actual values, never the declared types at a call site, so an absent value has no type at all, several constructors can match equally, and ambiguity surfaces at startup rather than in the build.

solid answer

~40 s

A compiler resolves a construction from the **declared** types of the argument expressions: it collects every applicable constructor and picks the most specific, and reports a tie as a build error. A container reading a manifest has no call site — only a type name and a list of existing objects. All it can read from an object is its runtime class, which is applicable to that class and to every supertype, so one value can fit several constructors and an absent value fits almost anything. The container then applies a tie-break the type's author never agreed to, often enumeration order. The fix is to stop matching on values: look the member up by name plus the full declared parameter-type list, so selection is exact and a signature change fails loudly.

code

pseudocode · 9 lines
pseudocode
type Report(source: ByteSource)     // candidate A
type Report(source: FileSource)     // candidate B; a FileSource is a ByteSource

openedFile = openFile("in.dat")     // runtime class of the value: FileSource

// manifest entry: { "type": "Report", "args": [openedFile] }

matchByValues("Report", [openedFile])           // A and B are both applicable
lookupBySignature("Report", ["ByteSource"])     // exactly A, no scan needed

go deeper

for a junior

Recall that building an object named in configuration is not the same act as writing the construction in source, and that the container has only the values, never the types written at a call site.

for a middle

Explain the mechanics: applicability against declared parameter types, a most-specific tie-break in the build versus an enumeration-order tie-break at run time, and why an absent value is applicable to almost everything.

for a senior

Show the production consequence: a new constructor overload re-points a manifest entry with a green build, and the wrong object only shows up in behaviour. Say how you would make the failure loud instead.

for a principal

Weigh exactness against ergonomics for every team on the platform: a signature-addressed manifest is verbose and breaks loudly, a value-matched one is pleasant and breaks quietly. Decide which failure you can afford.

## Two selections that share a name When source code constructs a `Report` from a `source` value, the choice of constructor is made before the program runs. The compiler reads the **declared** type of each argument expression, collects every constructor of `Report` that is *applicable* to those declared types under the conversions the language permits, and then picks the **most specific** of the applicable candidates. If two candidates are equally specific, the program does not build: the ambiguity is reported to the person who wrote the call, at the moment they can fix it. A wiring container that builds objects named in a startup manifest does the same job from a different input. It has a type name and a list of **values** — objects that already exist. There is no call site, no argument expression and no declared type anywhere in its input. Everything it can learn about an argument, it must read off the object in its hand. ## What a value does not tell you - A value's **runtime class** is a single type, but the parameter it was meant for may be that class or any supertype of it. A value whose runtime class is `FileSource` is applicable to a parameter declared `ByteSource` *and* to one declared `FileSource`, and nothing in the value says which the author meant. - An **absent value** carries no type at all. There is no runtime class to compare, so it is applicable to every reference parameter at once — the worst possible input to a scan. - A numeric value does not record whether its author wanted the wide parameter or the narrow one; where a runtime boxes numbers, one boxed value can widen into several declared parameter types. - **Parameter names** are not part of a value either, so a container that binds positionally cannot tell two same-arity, same-typed constructors apart by intent. ## How a run-time matcher actually chooses Two designs are in wide use and they behave very differently. 1. **Lookup by full signature.** The caller supplies the member's name *and* the list of declared parameter types. The runtime returns the one member declared with exactly that signature, or nothing at all. No applicability scan runs; selection is exact and total, and a signature that changed produces an immediate, obvious failure. 2. **Applicability scan over the values.** The caller supplies the name and the values. The container enumerates the candidates, keeps those whose parameters each accept the corresponding value, and applies its own tie-break — very often "the first one found", which is an enumeration order no type author ever promised to keep stable. | | Compile-time resolution | Run-time matching by values | |---|---|---| | Input to selection | declared types of the argument expressions | runtime classes of the actual values | | Absent argument | typed by its expression, participates normally | untyped, applicable to many candidates | | Tie-break | a most-specific rule fixed by the language | the matcher's own rule, often enumeration order | | Ambiguity appears | as a build failure, to the author | as a startup failure, or as a wrong pick | | Adding an overload | every call site is re-resolved and re-checked | nothing visible changes until a match shifts | ## The failure modes this produces 1. **A silent change of target.** Adding a constructor overload to a widely used type can re-point manifest entries nobody edited. The build stays green because no build step reads the manifest, and the first sign is a differently-shaped object at run time. 2. **Ambiguity as a run-time event.** What would have been a build error becomes a startup failure at best and a wrong object at worst — and it fires on the machine that loads the manifest, not on the machine where the type was edited. 3. **Absence that matches everything.** An entry that omits an optional collaborator can satisfy a constructor written for a completely different shape, because the missing value contradicts nothing. ## Making the choice deterministic - Address the member by **name plus the full parameter-type list**. Selection stops being a search, and a changed signature fails at the lookup instead of resolving quietly to something else. - Bind **by parameter name** where the runtime records names, so reordering two same-typed parameters does not silently swap two arguments. - Designate **one** wireable constructor per type, leaving an applicability scan nothing to be ambiguous about. - Validate the **whole manifest** before constructing anything, and make the failure message list the candidates considered and the reason each was rejected. A matcher that cannot explain its own choice cannot be debugged by the team that hit it.

  • What can a manifest entry carry so that constructor selection has only one outcome?
    The declared parameter types, not just the values. With the name and the full type list the runtime performs an exact lookup: it returns the single member declared with that signature or nothing, so there is no applicability scan and no tie-break. A later signature change then fails at the lookup instead of resolving to a different constructor.
  • Two candidates are equally applicable to the values you hold — what should the container do?
    Fail, and say why. It has no most-specific rule that the type's author agreed to, so any pick it makes is arbitrary and may change when the type is recompiled or its members are enumerated in a different order. The useful behaviour is to reject the entry during a validation pass and list both candidates in the message.
  • Why is adding a constructor overload a riskier change for a wired type than for one built in source?
    Source call sites are all re-resolved when the code is rebuilt, and an ambiguity stops the build. Manifest entries are not compiled and are not re-checked by anything, so a new overload can become the better match for an entry that nobody touched, and the change is invisible until the object behaves differently.

saying these in an interview costs you the question

  • Claims a run-time matcher applies the same most-specific rule the compiler applies.
  • Believes the argument values fully determine which overload the author meant.
  • Assumes an absent value can be matched to a parameter type like any other.
  • Says adding an overload cannot affect an existing name-based entry.
  • Thinks ambiguity is always reported before the program starts.