I recently wrote the following piece of code:
type _ t =
| Block : { field : int } -> [< `B | `I > `B] t
| Empty : [ `B | `I ] t
let narrow (x : [ `B | `I ] t) : [ `B ] t option =
match x with
| Block _ as y -> Some y
| Empty -> None
fully expecting it not to work (my previous forays into combining GADTs and polymorphic variants have never been successful). But to my surprise this is accepted by the typer without issues.
Of course if I actually use the variable in Block
then this is properly rejected:
type _ t =
| Block : { field : [< `B | `I > `B] as 'a } -> 'a t
| Empty : [< `B | `I > `I] t
let narrow (x : [ `B | `I ] t) : [ `B ] t option =
match x with
| Block _ as y -> Some y
| Empty -> None
$ ocaml file.ml
File "file.ml", line 7, characters 25-26:
7 | | Block _ as y -> Some y
^
Error: This expression has type [ `B | `I ] t
but an expression was expected of type [ `B ] t
The second variant type does not allow tag(s) `I
Given the error message, I assume that in the first example, the type of y
is a fresh variable of type [< `B | `I > `B]
, but I don’t really understand why it is OK for that variable to unify with [ `B ]
at toplevel.
The following is also accepted:
type _ t =
| K : int -> [> ] t
let transmute (x : [ `A ] t) : [ `B ] t =
match x with K _ as y -> y
On one hand, I am quite happy this works (I tested both of these using OCaml 5.1.1 and the alpha release of OCaml 5.3), because it seems useful. On the other hand, after discussing with @vlaviron, we are not quite sure whether this is intended behavior.
I know the interactions between GADTs and polymorphic variants are not very well understood; so I am wondering if a type system wizard could shine some light on what’s happening here. I am both interested in knowing whether this is sound and can be relied upon, and on the type-theoretic reasons for why it is so.