The brief answer is that GADTs make the type system too expressive to be fully inferred.
For instance, in your case, the following functions are both total (aka they handle all possible values of their input
let one = (One) => None
let two = (Two) => None
You can check that they are total by adding an explicit refutation clause in OCaml syntax (Reason syntax has not yet be updated to include those):
let one = function
| One -> None
| _ -> .
Here the dot .
means that the pattern described on the left-hand side of the clause is syntactically valid, but does not refer to any actual value due to some type constraints.
Consequently, you need to tell the type checker that you intend to match of a value of type t(a)
for any a
, this needs to be done with locally abstract types:
let x (type a, (x:t(a))) = switch(x){
| One => None
| Two => None
}
With this locally abstract annotation, the type checker knows that it is not supposed to replace this variable a
by a concrete type globally (aka it should consider that locally a
is some unknown abstract type), but it can refine it locally when matching a GADT.
Strictly speaking, the annotation is only needed on the pattern, thus you can write
let x (type a) = fun
| (One:t(a)) => None
| Two => None
Note that for recursive functions with GADTs, you may need to go for the full explictly polymorphic locally abstract type notations:
type t(_) =
| Int(int): t(int)
| Equal(t('a),t('a)):t(bool)
let rec eval: type any. t(any) => any = fun
| Int(n) => n
| Equal(x,y) => eval(x) = eval(y)
where the difference is that eval is recursively polymorphic. See https://caml.inria.fr/pub/docs/manual-ocaml-4.09/polymorphism.html#sec60 .
EDIT: Annotating the return type
Another annotation that is often needed to avoid the dreaded "this type would escape its scope" is to add an annotation when leaving a pattern matching.
A typical example would be the function:
let zero (type a, (x:t(a)) = switch (x){
| One => 0
| Two => 0.
}
There is an ambiguity here because inside the branch One
, the typechecker knows that int=a
but when leaving this context, it needs to choose one side of the equation or the other. (In this specific case, left at its own device the typechecker decides that (0: int)
is the more logical conclusion because 0
is an integer and this type has not been in contact in any way with the locally abstract type a
.)
This ambiguity can be avoided by using an explicit annotation, either locally
let zero (type a, (x:t(a))) = switch (x){
| One => ( 0 : a )
| Two => ( 0. : a )
}
or on the whole function
let zero (type a): t(a) => a = fun
| One => 0
| Two => 0.