Unification types variables and locally abstract types have completely different behaviors.
In particular, it useful to keep in mind that unification type variables:
- can be unified,
for instance
let f (x:'a) (y:'a) : 'a = ()
is valid and yields f: unit -> unit -> unit
.
are in scope for the whole toplevel definition.
For instance, the variable 'a
is the same across the whole scope of f
(which thus has type unit -> unit
)
let f x =
let () = (():'a) in
(x:'a)
Contrarily, locally abstract types are:
let f (type a) (x:a) (y:a) : a = ()
yields the expected
Error: This expression has type unit but an expression was expected of type a
- well-scoped, the local type cannot outlive its scope.
Typically,
let f x =
let y (type a): a = assert false
(* let's pretend that we can define such `y` value *) in
(x:a)
yields the expected out-of-scope error
Error: Unbound type constructor a
Due to those fundamental difference behaviors, locally abstract types have been extended to support more advanced features of the type systems.
Indeed, locally abstract types:
- can be used to define local modules, because they are type constructors and not type variables
let f (type a) (cmp:a -> a -> int) (x:a list) =
let module S = Set.Make(struct type t = a let compare = cmp end) in
x |> S.of_list |> S.elements
- can be refined with a local type equation when pattern matching on GADTs, because they have a well-defined scope:
type _ t =
| Int: int t
| Float: float t
let zero (type a) (x:a t) = match x with
| Int -> 0 (* in this branch a = int *)
| Float -> 0. (* whereas a = float in this one*)