The subtypes might be a solution, though I have to admit that I didn't get the full understanding of what you want. But later on it, let's first embrace polymorphic variants and subtyping. Your attempt is not using subtypes yet, as there is no polymorphism in your type, i.e., type t = ['A|'B|'C|'D]1 is a ground type. What we need is the following,
module Lib : sig
type 'a t = [< `A|`B|`C|`D] as 'a
val default : [ `B] t
val f : 'a t option -> unit
end = struct
type 'a t = [< `A|`B|`C|`D] as 'a
let default = `B
let f _ = ()
end
So we say that 'a Lib.t is a family of types, and a value of type 'a t could be ['A] or 'B or ['A|'B] or ['A|'B|'C] or ... with the [A|B|C|D] being the top type, aka the supertype.
With the default type we have options, we can post that it has type ['B] t, which is the same as ['B], but indicates more clearly that it is a part of a hierarchy, so the users should expect it to change to any other type. From the type system perspective, it doesn't matter, because the OCaml type system is not nominal but structural.
This solution will give you a type error here,
let config : _ Lib.t option =
match Lib.default with
| `A (* default is not polymorphic and can be only `B *)
| `B
| `C -> None
| `D -> Some `C
since we clearly stated that default is B and only B.
Alternatively, we can say that default could be [> 'B], i.e., that it is a polymorphic type that at least is B but could be anything else. With this solution, you will not get any errors in the config function. You will not either get any errors if you will change from [> 'B] to [> 'A], for example. So it is probably not what you're looking for, so let's go back and use the monomorphic ['B] type for the default and try to deal with it on the user side. We can explicitly say, that we want to upcast the ground default to the all possible values, e.g.,
module Lib : sig
type 'a t = [< `A|`B|`C|`D] as 'a
val default : [`B] t
val f : 'a t option -> unit
end = struct
type 'a t = [< `A|`B|`C|`D] as 'a
let default = `B
let f _ = ()
end
let config : _ Lib.t option =
match (Lib.default : [`B] Lib.t :> [> `B] Lib.t) with
| `A
| `B
| `C -> None
| `D -> Some `C
Now, if we change the default to A, we will have the desired type error. The only caveat is that we need to specify the currently verified default at every use case, so let's move it to the Lib instead, e.g.,
module Lib : sig
type 'a t = [< `A|`B|`C|`D] as 'a
type verified = [`B]
val default : [`B] t
val f : 'a t option -> unit
end = struct
type 'a t = [< `A|`B|`C|`D] as 'a
type verified = [`B] t
let default = `B
let f _ = ()
end
open Lib
let config : _ Lib.t option =
match (default : verified t :> [> verified ] t) with
| `A
| `B
| `C -> None
| `D -> Some `C
So now, when you would like to try a new default value, you change the type of default (and the value of course) but do not change the verified type and go through all use-cases until you're ready to add the newly added type to the verified set. Yes, set, because we can upgrade the verified type to accept a set of variants, e.g.,
module Lib : sig
type 'a t = [< `A|`B|`C|`D] as 'a
type verified = [`A |`B]
val default : [`B] t
val f : 'a t option -> unit
end = struct
type 'a t = [< `A|`B|`C|`D] as 'a
type verified = [`A|`B] t
let default = `B
let f _ = ()
end
open Lib
let config : _ Lib.t option =
match (default : [< verified] t :> [> verified ] t) with
| `A
| `B
| `C -> None
| `D -> Some `C
So now, we will get an error if Lib.default has any variant other than A or B. And as a bonus, you don't need to change anything on the use site.
And as the final refinement, I would suggest to get rid of nominal (in all senses of the word) 'a t type and just have to polymorphic types, one for the blessed set of verified constructors and another for the set of all possible constructors, e.g.,
module Lib : sig
type 'a default = [> `A|`B|`C|`D] as 'a
type 'a verified = [< `A |`B] as 'a
val default : [`B]
val f : 'a default option -> unit
end = struct
type 'a default = [> `A|`B|`C|`D] as 'a
type 'a verified = [< `A|`B] as 'a
let default = `B
let f _ = ()
end
open Lib
let config : _ option =
match (default : _ verified :> _ default) with
| `A
| `B
| `C -> None
| `D -> Some `C
or
let config : 'b option =
match (default : 'a verified :> 'b default) with
| `A
| `B
| `C -> None
| `D -> Some `C
1)) forgive me the wrong backticks, the right one do not play nice with SO markup