wiki

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].

§ 01

Example

Two records with a field x.

type a = { x : int }
type b = { x : string }
let f (r : a) = r.x
let 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 -> int
val 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 a
The first one was selected. Please disambiguate if this is wrong.
§ 02

The rule

type a = { x : int }type b = { x : string }let f (r : a) = r.xtype of r knownfrom the annotationx of alet g r = r.xtype of r unknownx of b, the lastignore (r : a); r.xknown from anearlier expressionx of awith -principal: warning 18
Three uses of the label x. An annotation on the argument selects a; with no information, the most recent record with the field is used; information that reaches the label only through an earlier expression also selects a, but depends on inference order.

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 type
int
§ 03

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.

§ 04

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 = struct
type t = Circle of float | Rect of { w : float; h : float }
end
(* Neither the constructors nor the fields are in scope here; the expected
type 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.h
let 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 | Spades
type card_back = Spades | Plain
let is_black (s : suit) = s = Spades
let back : card_back = Spades
let default = Spades

What ocamlc -i infers.

type suit = Hearts | Spades
type card_back = Spades | Plain
val is_black : suit -> bool
val back : card_back
val 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.

§ 05

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 where
data A = A { x :: Int }
data B = B { x :: String }
f :: A -> Int
f r = r.x
-- No annotation: rather than pick a type, GHC abstracts over any record
-- with a field x.
g r = r.x
main :: IO ()
main = do
print (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.

§ 06

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

referenced by

further reading

  1. [1]OCaml 4.01.0 release notes (2013), which introduced type-based selection of record labels and constructors.
  2. [2]OCaml manual, “Records and variants”, section on type-directed disambiguation.
  3. [3]J. Garrigue, D. Rémy, “Semi-explicit first-class polymorphism for ML”, Information and Computation 155 (1999).
  4. [4]B. C. Pierce, D. N. Turner, “Local type inference”, ACM Transactions on Programming Languages and Systems 22 (2000).
  5. [5]GHC User’s Guide, the DuplicateRecordFields and OverloadedRecordDot extensions.
  6. [6]The Rust Reference, “Field access expressions”.

last updated