Abstract type that is a polymorphic variant


#1

How can I represent a module interface that defines an abstract type that is supposed to be a polymorphic variant?

module type TextContentF = {
    type t = pri string;
    type errs; /* This is a poly-var */
    let make: string => D.t(t, [> errs]); /* I want to be able to specify that make() can return at least the errors */
}

I’m getting error:

Error: The type errs does not expand to a polymorphic variant type

#2

Define the polyvariant type in the interface:

type errs = [`err1 | `err2];

See e.g. https://github.com/yawaramin/prometo/blob/ef7229d3cbfe9980e32feef8e882785b49a96434/src/Yawaramin__Prometo.rei#L6


#3

I should mention I’m using this interface for a functor implementation so any text decoding modules implement this interface and can give their own errors. Therefore I do not know the errors ahead of time which is why I’m using an open polymorphic variant.

I’m trying to do Section 4 Functors for extending modules: https://2ality.com/2018/01/functors-reasonml.html

module type TextContentI = {
  type t = pri string;
  type errs;
  let make: string => D.t(t, [> errs]);
};
module TextContent = (X: TextContentI) => {
  type t = X.t;
  type errs = X.errs;
  let make = X.make;
  let apply = (f, s) => f(s);
  let toJs = (t: t): string => (t :> string);
  let toDb = (t: t): string => (t :> string);
};

Where TextContentI is some implementation of a string decoder that can return various errors as poly vars.


#4

You don’t need to define all the polyvariant tags ahead of time, just one. This proves to the compiler that it’s a polyvariant type. Users can add more later since the make function’s error type is open.


#5

Ok I had tried something like that and found a way to coerce the types to match but it turns out I had another problem causing the make function to return a closed poly var constraint rather than open.

I was using reazen/relude’s NonEmpty.List module to contain a list of “errs” and I had to implement my own non-empty list type to get it to work.

module type NonEmptyList = {
  type container('a) = list('a); /* If this line is abstract (as in just "type container('a);" it causes problems */
  type t('a) = option(container('a));
  /* ... */
}

Furthermore my

D.t('a, 'b)

type expanded to

result(('a, array('b)), list('b));

And if array was used to contain 'b, it caused the poly vars to always infer as closed.

So if any of the above were true (if I used array type to contain the polyvars instead of list), it would cause

let make: string => D.t(t, [> errs]);
/* D.t('a, [ `Specific1Error ]) */

Whereas if I changed to list type, removed the abstract type in the interface to explicitly define list, then the type would be inferred as

/* D.t('a, [> `Specific1Error ]) */

Then that lets me do the workaround given in this answer: https://stackoverflow.com/a/53930341


#6

TextContentI is being used as an interface for X, which involves other modules that define their own unique non-intersecting errs type. So I tried

module type TextContentI = {
    type errs = [ | `BaseError ];
...
};

But then any module that uses TextContentI type has to have the exact same “type errs;” and it gives a … not included in type errs = [ | `BaseError ]; error.