Note: I’m going to use the error messages which will be printed by the next version of the compiler, because the formatting is much nicer (courtesy of @Armael), but the error message itself is unchanged since at least 4.03, so the version of OCaml doesn’t really matter here.
So, first: I think we all agree that the following error message is terrible (and hopefully we’ll improve it some day):
19 | | Sst (c,k) -> eval_com_cont arg c k
^^^
Error: This expression has type state but an expression was expected of type
'a
This instance of state is ambiguous:
it would escape the scope of its equation
Funnily enough, if we invoke the compiler with -short-paths
(which is what dune does I believe), the error message becomes:
19 | | Sst (c,k) -> eval_com_cont arg c k
^^^
Error: This expression has type a but an expression was expected of type 'a
This instance of a is ambiguous:
it would escape the scope of its equation
And that actually gives some hint about what the error actually is: arg
at this point has type a ≈ state
(which we call an ambivalent type), but that type only exists in the scope of the branch (where the equality is introduced by matching on Sst
). If you want to pass arg
to another function, a choice needs be made regarding which side of the equation you want to use as the type for arg
, and in the code you gave in your first message, the compiler doesn’t have enough information to make that choice: it is ambiguous.
The first solution given by @ivg resolves that ambiguity by telling the compiler "choose the type state
", and of his two proposed solutions, that one is indeed the more robust, and in my opinion, the one you should use.
The reason why his second proposition (the reordering) has any effect (that is: solves your problem), is the same reason explaining why @octachron’s proposition also solves the problem, and is sort of an implementation detail: when you have a group of mutually recursive functions, the compiler will start by approximating their type, before actually typing them (because when you start typing apply
and you encounter the use of eval_com_cont
you need to have a guess at what its type actually is).
So, assuming you had something like
let rec apply arg k = ...
and eval_com_cont st c cont = ...
the compiler would assign the type 'a -> 'b -> 'c
to apply
and 'd -> 'e -> 'f -> 'g
to eval_com_cont
before typing apply
, and after that, when it starts typing eval_com_cont
it will use the proper type of apply
that it has just computed, but will still use the “approximation” of the type of eval_com_cont
.
That is why the reordering has an impact, if you reorder then when you type apply
, you already know that eval_com_cont
has type state -> com -> state cont -> 'b
, and you can without a doubt choose to use state
as the type for arg
.
Now, the reason why @octachron’s solution works, i.e. having eval_com_cont : state -> _ -> _ -> _ = fun st c cont -> ...
(that is enough btw, you don’t need to expand any more), is that in this particular case, the compiler will look at the type annotation when approximating the type of the function, whereas it doesn’t when you write eval_com_cont (st : state) c cont = ...
.
That is a bit disappointing, and we could probably look into making it better.
I think that’s the full explanation. If you want to know more about ambivalent types, this paper from Garrigue and Rémy contains all there is to know (and if, you omit the formal development, is very accessible).