8.4 Overloading and Type Inference Basics
Two classic sources of type-system power are overloading and type inference. Both reduce source-code repetition, but both also force the compiler to reason from incomplete information.
Overloading asks: “this name has multiple meanings; which one fits here?” Type inference asks: “this program omitted some type annotations; what constraints force the missing type?”
In both cases, the compiler should be explainable. Hidden tie-breakers and magical guesses eventually become maintenance problems.
Overloading Needs a Ranking Rule
Suppose a language has:
print(int)
print(float)When the user writes print(3), the resolver should prefer the exact int match. When the user writes print(3.0), it should prefer the float version. When the call fits multiple candidates equally well, the correct response is usually an ambiguity diagnostic rather than a silent guess.
That gives a simple ranking principle:
| candidate relation | usual priority |
|---|---|
| exact match | best |
| requires acceptable widening | next |
| requires risky conversion | worse |
| tied with no unique best | report ambiguity |
Type Inference Collects Constraints
Inference is often described as if the compiler “guesses” a type. That is misleading. A better picture is that the compiler creates type variables, gathers constraints, and unifies them.
For example:
let x = 1starts with something like x : ?T. The literal contributes 1 : int. The assignment forces ?T = int. After unification, the compiler concludes x : int.
The same pattern scales upward:
- function parameters create unknowns,
- return expressions constrain them,
- call sites instantiate them,
- and composite constructors propagate them.
Inference Has Boundaries
Even languages with strong inference still ask for explicit annotations in some places. Common reasons include:
- a recursive definition does not constrain itself enough,
- a public API should expose a stable contract,
- overload resolution would become ambiguous,
- or the inferred result would be correct but too hard for humans to read.
This is an important engineering lesson: inference and annotations are not enemies. An annotation is often a clarity tool as much as a compiler aid.
Worked Comparison
Compare these two declarations:
let id = (a) => a
let parseId: (string) -> UserId = ...The first benefits from inference because the identity shape is simple and local. The second benefits from an explicit annotation because it defines a boundary that other modules will depend on. Strong language design chooses where inference improves clarity and where omission would make the program harder to understand.