Type-directed disambiguation
Two record types in one module can have a field of the same name, and two variant types a constructor of the same name. Before OCaml 4.01 the later definition simply shadowed the earlier one, so the earlier type's field could not be used by name. Since 4.01, the type checker uses what it already knows about an expression's type to choose[1][2]. If the type is known, from an annotation, a function's argument type or an earlier use, the label is looked up in that type, even if the label is not in scope; if not, the most recent definition is used, as before. This is a form of bidirectional type checking, in which an expected type flows into an expression[4]. It makes duplicate field names practical and lets code use a type's labels and constructors without opening its module, but it makes the result depend on the order of type inference, which OCaml's principal mode checks[3].
Example
Two records with a field x.
type a = { x : int }type b = { x : string }let f (r : a) = r.xlet g r = r.x
What ocamlc -i infers. The annotation picks a; with nothing known, the last definition wins.
type a = { x : int; }type b = { x : string; }val f : a -> intval g : b -> string
With warning 41 enabled, the ambiguity in g is reported.
File "records.ml", line 5, characters 12-13:5 | let g r = r.x^Warning 41 [ambiguous-name]: x belongs to several types: b aThe first one was selected. Please disambiguate if this is wrong.
The rule
When the type checker meets a record field access e.l, a record construction, or a constructor in an expression or pattern, it first looks at the expected type, the type that the context already requires[2]. If that type is known to be a record or variant type, the label or constructor is looked up in its definition. Otherwise it resolves the name by scope, choosing the most recent definition, and the expected type then has to agree with it.
“Already known” means known at that point of inference, which runs roughly left to right. A label read before anything has fixed its record's type resolves to the most recent record with that field, and the error then names a type the code never meant:
The label is resolved before the addition says it must be an int.
type a = { x : int }type b = { x : string }let h r = r.x + 1
The compiler's error.
File "order.ml", line 4, characters 10-13:4 | let h r = r.x + 1^^^Error: This expression has type string but an expression was expected of typeint
Principality
Type inference in ML has principal types: the result does not depend on the order in which constraints are solved. Type-directed disambiguation breaks this if information reaches a label through a path that depends on that order, such as an earlier statement that happened to be checked first. OCaml's -principal mode tracks which type information is known in an order-independent way, using the technique Garrigue and Rémy introduced for polymorphic methods[3], and warns when a disambiguation relied on anything else.
The record type is fixed by an earlier expression.
type a = { x : int }type b = { x : string }let h r = ignore (r : a); r.x + 1
What ocamlc -i infers.
type a = { x : int; }type b = { x : string; }val h : a -> int
The same file compiled with -principal.
File "flow.ml", line 4, characters 28-29:4 | let h r = ignore (r : a); r.x + 1^Warning 18 [not-principal]: this type-based field disambiguation is not principal.
An annotation on the parameter itself, (r : a) in the function header, gives the same result with a principal derivation.
Names out of scope
Because the expected type identifies the type definition, labels and constructors can be used even when they are not in scope, without opening or qualifying their module. This is convenient for pattern matching on types from other modules.
Matching on a type from another module without opening it.
module Shape = structtype t = Circle of float | Rect of { w : float; h : float }end(* Neither the constructors nor the fields are in scope here; the expectedtype Shape.t tells the type checker where to find them. *)let area (s : Shape.t) =match s with| Circle r -> 3.14159 *. r *. r| Rect r -> r.w *. r.hlet unit_square : Shape.t = Rect { w = 1.; h = 1. }
Running it.
3.14 1.00
Warning 40, off by default, reports each name selected out of scope.
File "scope.ml", line 9, characters 4-10:9 | | Circle r -> 3.14159 *. r *. r^^^^^^Warning 40 [name-out-of-scope]: Circle was selected from type Shape.t.It is not visible in the current scope, and will not
Constructors are disambiguated in the same way:
A constructor name shared by two variant types.
type suit = Hearts | Spadestype card_back = Spades | Plainlet is_black (s : suit) = s = Spadeslet back : card_back = Spadeslet default = Spades
What ocamlc -i infers.
type suit = Hearts | Spadestype card_back = Spades | Plainval is_black : suit -> boolval back : card_backval default : card_back
It is also what lets code generated without types work: a ppx that writes s.address.city with only s annotated leaves the type checker to find which record each field belongs to.
In other languages
Haskell's record fields were originally top-level functions, so two records in one module could not share a field name. DuplicateRecordFields allows it, and OverloadedRecordDot, added in GHC 9.2, makes r.x a use of the HasField class[5]. Where OCaml picks a type, GHC abstracts over it: a function that reads x from an unknown record gets a constraint instead.
The same two records in Haskell.
{-# LANGUAGE DuplicateRecordFields, OverloadedRecordDot #-}module Main wheredata A = A { x :: Int }data B = B { x :: String }f :: A -> Intf r = r.x-- No annotation: rather than pick a type, GHC abstracts over any record-- with a field x.g r = r.xmain :: IO ()main = doprint (f (A 1))print (g (A 2), g (B "three"))
Running it.
1(2,"three")
The type GHC infers for g.
g :: GHC.Records.HasField "x" r a => r -> a
Rust resolves a field access by the type of the expression, which must already be known at that point; otherwise the compiler asks for a type annotation[6]. Scala and other languages with methods resolve fields as members of the receiver's class, which again requires the receiver's type.
History
Record labels in Caml and OCaml were originally resolved purely by scope, like other names, so a label shadowed any earlier one of the same name. Type-based selection of labels and constructors arrived in OCaml 4.01 in 2013, together with warnings 40, 41 and 42 for out-of-scope, ambiguous and disambiguated names[1]. The principal mode, which predates it and was designed for polymorphic methods and labeled arguments[3], was extended to report non-principal disambiguations as warning 18. GHC added duplicate record fields in version 8.0 in 2016 and record dot syntax in 9.2 in 2021[5].
see also
- ppxA preprocessor that rewrites the OCaml syntax tree between parsing and type checking. Derivers generate code from type declarations tagged [@@deriving name]; extension points expand nodes written [%name payload]. Neither sees any types.
- Polymorphic variantA polymorphic variant is a variant whose constructors, called tags and written with a backquote in OCaml, exist independently of any type declaration. Their types are sets of tags with bounds: [> `A] means at least `A, [< `A | `B] means at most those two, and unification works out unions and intersections of those sets using row variables. They let functions share constructors across types, be extended with new cases without editing old code, and encode subtyping, at the cost of longer types and error messages and of typos in open types going unnoticed.
- Value restrictionThe value restriction is the rule, in ML-family languages, that a let-bound expression has its type variables generalized only if it is a syntactic value, such as a function, a constant or a constructor applied to values. An application such as List.map f or ref [] is not a value, so its type variables stay weak and are fixed by their first use. Without the restriction, a reference could be given a polymorphic type and used to store a value at one type and read it at another. OCaml relaxes the rule for type variables that occur only covariantly.
- Overlapping instancesTwo type class instances overlap when some type matches both of their heads, as Pretty [a] and Pretty String do at [Char]. Instance resolution in Haskell does not backtrack, so by default GHC reports an error rather than choose. The OVERLAPPING and OVERLAPPABLE pragmas let the most specific matching instance win, which is predictable when the choice is made at a known type and all the instances are in scope, but can make the same expression behave differently in different modules. INCOHERENT lets GHC choose even when the choice depends on a type it does not yet know. Closed type families and newtypes are the usual alternatives.
referenced by
further reading
- [1]OCaml 4.01.0 release notes (2013), which introduced type-based selection of record labels and constructors.
- [2]OCaml manual, “Records and variants”, section on type-directed disambiguation.
- [3]J. Garrigue, D. Rémy, “Semi-explicit first-class polymorphism for ML”, Information and Computation 155 (1999).
- [4]B. C. Pierce, D. N. Turner, “Local type inference”, ACM Transactions on Programming Languages and Systems 22 (2000).
- [5]GHC User’s Guide, the DuplicateRecordFields and OverloadedRecordDot extensions.
- [6]The Rust Reference, “Field access expressions”.
last updated