File using poly variants compiles in Reason but does not compile in Rescript

This compiles fine in Reason/Ocaml

let f: unit => [ | `A] = () => `A;
let g: unit => [ | `B] = () => `B;

let h = b =>
  if (b) {
    (f(): [ | `A] :> [> | `A]);
  } else {
    (g(): [ | `B] :> [> | `B]);
  };

but does not compile in Rescript

let f: unit => [#A] = () => #A
let g: unit => [#B] = () => #B

let h = b =>
  if b {
    (f(): [#A] :> [> #A])
  } else {
    (g(): [#B] :> [> #B])
  }
Type Errors
[E] Line 8, column 5:
This has type: [#B]
  Somewhere wanted: [#A]
  These two variant types have no intersection

Interesting example! I think the Reason translation of the code is subtly different though. If you check the literal Reason translation of this ReScript sample, it has some extra parentheses:

let h = b =>
  if (b) ((f(): [`A]) :> [> `A]) else ((g(): [`B]) :> [> `B]);

Voila–this version also gives the same error, even in Reason syntax. The ReScript syntax has subtly different precedence and associativity for parsing the type annotation and upcast symbols; by default it interprets them as (exp: TYPE1) :> TYPE1 whereas Reason and OCaml interpret them as exp: (TYPE1 :> TYPE2). The problem is there’s no way to tell ReScript to do the latter as well–parentheses around the (TYPE1 :> TYPE2) are a parse error.

This could plausibly be filed as a syntax issue. In the meantime it should be fairly simple to work around, by not using type annotations. The compiler’s type inference is good enough to just figure out what the correct types should be:

let f = () => #A
let g = () => #B
let h = b => if b { f() } else { g() }

If you need annotations for documentation purposes, it’s better to learn the rules of polymorphic variant type inference. E.g., in this case, the correct way to write the types would be:

module M: {
  let f: unit => [> #A]
  let g: unit => [> #B]
  let h: bool => [> #A | #B]
} = {
  let f = () => #A
  let g = () => #B
  let h = b => if b { f() } else { g() }
}
1 Like

Well, I should have specified that the Rescript code is a result of automatic conversion using bsc -format ...

Ah, in that case definitely something that should be reported as an issue.

1 Like
module M: {
  let f: unit => [> #A]
  let g: unit => [> #B]
  let h: bool => [> #A | #B]
} = {
  let f = () => #A
  let g = () => #B
  let h = b => if b { f() } else { g() }
}

My original (not-reduced) use case is more complicated and includes module interfaces/functors/composable error handling etc with functions f and g being callbacks for certain stages of the pipeline.

Currently (I might be wrong) it seems that going the way of constraints would be an invitation to the “world of hurt”. I am conscientiously trying to not use bounds for those functions (sorry for the poor explanation, still working on the understanding of ergonomics)

I understand, I would still recommend to learn the rules of how polymorphic variant types are inferred from definitions. Composeable error handling with polyvariants only works when they have open bounds so the compiler can compose them. If they don’t, you could end up with ugly (and brittle) code that would be difficult to understand and change. With open bounded polyvariants, it’s still somewhat difficult to understand but at least not densely packed with symbols and a bit easier to read.