Fixing type inference when record types have same labels

Viewed 102

Consider the following record types:

type t1 = {
    label1: string option
  ; label2: (string * int) list
  ; label3: (string * int) list
  ; }

type t2 = {
    label1: string option
  ; label2: (string * int) list
  ; label3: ((string * int) list) list
  ; }

I know that I cannot define types t1 and t2 in the same module, because label1 and label2 have the same types and are defined both in t1 and t2, which will break type inference. Thus, if changing labels is out of question, I have two options:

I can either use module namespaces:

module M1 = struct
    type t1 = {
      label1: string option
     ; label2: (string * int) list
     ; label3: (string * int) list
     ; }
end

module M2 = struct
   type t2 = {
       label1: string option
    ; label2: (string * int) list
    ; label3: ((string * int) list) list
    ; }
end

Or I can specify the type in each and every function defined in the same file:

let f1 (param: t1) = ...

let f2 (param: t2) = ...

let f3 (param: t1) = ...

What are the pros and cons of each approach?

1 Answers

First of all, it will not break type inference and you can define multiple records with the same fields as well as multiple variants with the same constructors in the same module. It is not recommended in general but is perfectly fine and well-defined.

When you define two records that share some field names, the last definition of a field hides the previous definitions, e.g.,

type t1 = {x : int}
type t2 = {x : char}

let x {x} = x

The x function will have type t2 -> char. It is because type inference is syntax-driven and from the name of a field it goes to the type of its record. In modern versions of OCaml, you can use type-directed constructor disambiguation to access the shadowed fields, e.g.,

let x : t1 -> _ = fun {x} -> x

Since we constrained the type, the compiler will look for the field in the specified record.

This is a rather new feature and before it, there were only two options for field name disambiguation. In fact, both are still the options and probably are recommended.

The first option is to put each record in its own module. This is my personal preference. In fact, I don't like having records or variants in my interfaces at all and always pack them in modules and hide them under interfaces as an implementation detail.

module T1 = struct
  type t = {x : int}
end

module T2 = struct
  type t = {x : char}
end

let x_of_t1 {T1.x} = x
let x_of_t2 {T2.x} = x

But it is even better to provide val x : t -> _ function in each module and use the module interface without relying on the internal representation.

The next and the last option is to use good old and ugly prefixes to disambiguate the names. Not really a solution (as the names are just now different) but it is quite common and is not that bad as it looks like, e.g.,

type t1 = {t1_x : int}
type t2 = {t2_x : char}

I personally dislike this solution, but it is quite common amongst various program analysis projects where you have lots of intermediate languages all sharing the same constructs but with different payloads. E.g., a constant Pconst of string * loc, which comes from the parse tree, vs. a constant in the abstract syntax tree (AST) Aconst of int * loc vs. a constant in the typed AST Tconst of int * typ * loc, and so on. Using prefixes here is practical, cf. T.Const vs Tconst, an economy of one character! :), and well-established tradition. Though I would personally still prefer modules to all other options.

Related