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?