franky
franky

Reputation: 1333

Possible to annotate composed polymorphic variant type?

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

Answers (2)

Shon
Shon

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

glennsl
glennsl

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 to your question since it has several followers with significant knowledge that can hopefully put the final pieces together.

Upvotes: 2

Related Questions