Reputation: 1333
I've been using polymorphic variants for error handling with result types (taken from http://keleshev.com/composable-error-handling-in-ocaml) and it's been great for exhaustive checking. I recently came across the need to annotate the result type in a functor but not sure if it's possible. This is a snippet with some comments on what I'm trying to accomplish:
open Belt;
/* A quick example of what's working for us right now. This is fine and
the result error type is [> `Error1 | `Error2 ] */
let res = Result.flatMap(Result.Error(`Error1), _ => Result.Error(`Error2));
/* A really generic version of what what we're trying to do */
module type General = {
type t;
type error;
let res: Result.t(t, error);
};
module Make = (M: General) => {
let res = M.res;
};
module Specific1 =
Make({
type t = string;
type error = [ | `Specific1Error];
let res = Result.Error(`Specific1Error);
});
module Specific2 =
Make({
type t = int;
type error = [ | `Specific2Error];
let res = Result.Error(`Specific2Error);
});
/* This definitely doesn't compile because the two error types
aren't the same but wondering if anything above can be changed so it
understands the error type is [> `Specific1Error | `Specific2Error] */
let res = Result.flatMap(Specific1.res, _ => Specific2.res);
Upvotes: 2
Views: 294
Reputation: 4098
Is there a reason you need the error
type sealed inside your General
module signature? Since it looks like you'll need to know the sum of all these errors variants to annotate the final res
value in any case, can you do the following? (Please excuse the translation to standard OCaml syntax and idioms.)
open Core
type error =
[ `Specific1Error
| `Specific2Error
]
module type General = sig
type t
val res : (t, error) Result.t
end
module Make (M: General) = struct
let res = M.res
end
module Specific1 =
Make (struct
type t = string
let res = Result.Error `Specific1Error
end)
module Specific2 =
Make (struct
type t = int
let res = Result.Error `Specific2Error
end)
(* This type expands to
* (int, [ `Specific1Error | `Specific2Error ]) result
*)
let res : ('a , error) Result.t =
let open Result.Monad_infix in
Specific1.res >>= (fun _ -> Specific2.res)
(* These errors will still compose with others down the line: *)
type error_plus = [error | `More_errors ]
Upvotes: 0
Reputation: 29106
This isn't a complete answer, but it provides a bit more information and one possible solution or workaround.
It's possible to get the last line to compile by adding explicit coercion to the specific combined type:
let res =
Result.flatMap(
Specific1.res :> Result.t(string, [`Specific1Error | `Specific2Error]),
_ => (Specific2.res :> Result.t(int, [`Specific1Error | `Specific2Error])));
Here they're both coerced to the same type, so we're all good. As for why it needs to be explicit, my understanding is that this is to prevent accidental mistakes from mistyping constructors. See more in this answer.
If type error
had specified a lower bound we wouldn't have to be explicit, as demonstrated by this:
let error1 : Result.t(int, [> `Error1]) = Result.Error(`Error1);
let error2 : Result.t(int, [> `Error2]) = Result.Error(`Error2);
let res = Result.flatMap(error1, _ => error2);
But since upper and lower bound polymorphic variants have an implicit type variable, we would at the very least have to change type error
to type error('a)
. Unfortunately even then I'm not sure how to get the module signature to line up with the implementations since, for example, this:
type error('a) = [> | `Specific1Error] as 'a;
fails with
Signature mismatch:
...
Type declarations do not match:
type 'a error = 'a constraint 'a = [> `Specific1Error ]
is not included in
type 'a error
Their constraints differ.
It's also not possible to coerce to a lower bound polymorphic variant type, and I'm not sure why that is either:
let res =
Result.flatMap(
Specific1.res :> Result.t(string, [> `Specific1Error]),
_ => (Specific2.res :> Result.t(int, [> `Specific2Error])));
fails with
This has type:
(int, [ `Specific2Error ]) Result.t
But somewhere wanted:
(int, [ `Specific1Error ]) Result.t
These two variant types have no intersection
indicating that the bounds are simply ignored.
I've reached the limits of my knowledge here on several fronts, but I've added ocaml to your question since it has several followers with significant knowledge that can hopefully put the final pieces together.
Upvotes: 2