I’ve been playing around with the idea of modeling FSMs with GADTs and polymorphic variant constraints.
The basic idea looks as follows:
type state = [ `foo | `bar ]
type ('from, 'to_) event =
| First :
([ `initial ], [ `foo ]) event
| Second : ([ `foo ], [ `bar ]) event
| Third : ([ `bar ], [ `terminal ]) event
type _ step =
| I : [ `initial ] step
| S : 'a step * ('a, [< state ] as 'b) event -> 'b step
| T : 'a step * ('a, [ `terminal ] as 'b) event -> 'b step
type t = M : _ step -> t
Here, state represents a possible state of the FSM, an event is what allows for transitions between states, and step is the glue that holds it all together.
Ideally, this looks something like:
let _ = M (T (S (S (I, First), Second), Third))
which is a transition from the initial state to the terminal one.
This works pretty well actually, except when I’ve been pattern matching over t. The compiler (rightly) does not allow something like this to be constructed
let _ = M (T (S (I, First), Third))
It gives the error
Error: This expression has type ([ `bar ], [ `terminal ]) event
but an expression was expected of type ([ `foo ], [ `terminal ]) event
These two variant types have no intersection
However, when pattern matching over values of t, the compiler refuses to refute such impossible cases:
let f = function
| M (S (_, First)) -> ()
| M (S (_, Second)) -> ()
| M I -> ()
| M (T (S (_, Second), Third)) -> ()
| M (T (S (_, First), Third)) -> .
It gives the error
Error: This match case could not be refuted.
Here is an example of a value that would reach it:
M (T (S (I, First), Third))
Am I doing something wrong here or is this just a limitation of using polymorphic variant constraints with GADTs?