Unnecessarily strong type constraints with polymorphic variants (when using `React.useState`?)

The following works (ReScript Playground):

let f = v => {
  let x = ref(#b)
  x := v
}
let g = () => {
  let x = #a
  switch x {
  | #a => f(x)
  }
}

but the equivalent code using React.useState does not (ReScript Playground):

let f = v => {
  let (_x, setX) = React.useState(() => #b)
  setX(_ => v)
}
let g = () => {
  let (x, _setX) = React.useState(() => #a)
  switch x {
  | #a => f(x)  /* This has type: [#a]
  But this function argument is expecting: [> #b]
  The first variant type does not allow tag(s) #b */
  }
}

I can explicitly annotate x: [> #a | #b] in g, but it makes the switch non-exhaustive, and moreover, it is not a general solution as x can no longer be applied with other functions accepting different poly variants, e.g., [> #c].

What is causing this problem? How can I bypass this?

1 Like

That what you’re perceiving as a problem is exactly by design. Variants (polymorphic or not, I’m using the term loosely in this context) will always require exhaustive checks/pattern matching. Inversely, singular variants will be their own type and will not be interchangeable with each other. i.e type a = [#a] is not the same type as type b = [#b]. That is what your second example defines - the set function inside g() accepts only type [#a].

However, I find this behavior puzzling:
Example 1: this should have been an error, but is not
Example 2: whereas this is an error

I suspect my example 1 and your first example’s f function

let f = v => {
  let x = ref(#b)
  x := v
}

has something to do with block scoping and the rule “The value of the last line of a scope is implicitly returned.” and how the type inference deduces the types in a block scope vs the types in a module. Can someone else explain if this behavior is correct?

I don’t think my first example is an error; I wrote it that way deliberately to make the typechecker infer the type of x in f to be ref<[> #b]>, and the type of x in g be [> #a]. That way, f can accept something more than #b, while inside g, the switch expression is still exhaustive. I believe this is because the type checker unifies the types with the most general types.

That is why I am puzzled with the second example, as I wrote it (almost) the same way as I have done with the first example—so I think the second example is misleading for me. What’s more puzzling is that the following type checks:

let f = v => {
  let (_x, setX) = React.useState(() => #b)
  setX(_ => v)
}
let g = () => {
  let (x, _setX) = React.useState(() => #a)
  f(x)
  /* switch x {
  | #a => f(x)
  } */
}

and this too:

let f = v => {
  let (_x, setX) = React.useState(() => #b)
  setX(_ => v)
}
let g = () => {
  let x = #a
  switch x {
  | #a => f(x)
  }
}

I don’t feel like an expert on this topic, but I think I’m able to explain the issue:
I adapted your example in the playground to not use React.useState but a custom function having the same signature:

/* previous example given by Zeta611
let f = v => {
  let x = ref(#b)
  x := v
}
*/

module State = {
  /*
  naive implementation of usestate
  
  mind that this implementation probably does not have the desired runtime behaviour,
  but has the same type signature as React.useState
 */
  let use: (unit => 'a) => ('a, ('a => 'a) => unit) = init => {
    let x = ref(init())
    let setX = f => x := f(x.contents)
    (x.contents, setX)
  }
}

/* infered as [> #b] => unit */
let f2 = v => {
  let (_x, setX) = State.use(() => #b)
  setX(_ => v)
}

let g = () => {
  let (x, _setX) = State.use(() => #a) // x is infered as: [#a]
  switch x {
  | #a => f2(#a)
  | #a => f2((x :> [#a | #b]))
  /* following fails with: 
  | #a => f2(x)
	  This has type: [#a]
  		But this function argument is expecting: [> #b]
  		The first variant type does not allow tag(s) #b
 */
  }
}

The Problem

  1. setX inside of the function f2 infers as ([> #b] => [> #b]) => unit:

    • init function sets x just to #b
    • f2’s argument v is inferred as any polymorphic variant constructor (due to x being a polymorphic variant)
    • hence the compiler will infer a lower bound for x: [> #b] - “at least the constructor b
  2. x inside of the function g infers as [#a] (without any bounds):

    • init function sets x just to #a
    • there isn’t any possibility of x being something else than #a
    • hence the compiler will infer just #a - “x can only hold the constructor a
  3. the error message This has type: [#a] But this function argument is expecting: [> #b] The first variant type does not allow tag(s) #b:

    • when you call f2(x) in the function g, you are saying “pass x” (typed as [ #a ]) “to a function f2” (typed as [> #b] => unit)
    • a value of type [ #a ] can never have the shape of #b, while [> #b ] requires the expression to be “at least” possible to take on the shape of #b. which is not possible for x in g - hence the error message

Possible Solutions

I. One possible solution to this problem - although it might not be what you’re looking for exactly - is to call f2 with the same constructor you already pattern matched on. This way the newly created value will be inferred as [> #a | #b ] and that’s a type you are able to pass to f2.

II. Another possible solution would be to coerce the type of x inside of g like this: f2((x :> [#a | #b])) - “overruling” the inference.

III. Alternatively you should think about your use case, and if other thechniques would be a better fit.

It could be easier to recommend a fitting solution, knowing more about your actual use case:

  • Who would consume the actual state you store in f?
  • Why do you need polymorphic variants? What are you trying to achieve?
3 Likes

Thank you for the thorough reply, I really appreciate it!

However I am still puzzled by:
" * there isn’t any possibility of x being something else than #a"
If I slightly modify g:

let g = () => {
  let (x, _setX) = State.use(() => #a) // x is infered as: [> #a]
  switch x {
  | _ => f2(#a)
  }
}

x is now infered as [> #a]. I suspect the exhaustiveness check of the switch cases forces x to have a closed type [ #a ]; I could reproduce the same behavior with an OCaml version of this code. I’m not really sure what this “feature” is trying to prevent.

My use case is… again with GraphQL, where my React component only uses [ #asc | #desc ], while the ppx-generated MyQuery.use({orderBy: orderBy}) expects me to pass

[
  | #desc_nulls_last
  | #desc_nulls_first
  | #desc
  | #asc_nulls_first
  | #asc_nulls_last
  | #asc
]

So probably it is best to alias the above lengthy type to something like order and use :> order.
I’m not completely satisfied with this solution, as the use of the GraphQL API in a separate function leaks the implementation detail, contaminating other functions.