Enforce the same generic type between two functions

I’m writing bindings for a function that takes a string.

@send
external scene: (t, string, 'a => unit) => unit = "scene";

@send
external go: (t, string, ~data: 'a=?) => unit = "go";

Usage is as follows:

k->scene("one", () => {});
k->go("one");

These strings represent a limited set of values. One could model them with a variant:

type scenes = | One | Two;

Can I keep the type generic in my bindings while enforcing a relationship between the two functions?

@send
external scene: (t, 'scene, 'a => unit) => unit = "scene";

@send
external go: (t, 'scene, ~data: 'a=?) => unit = "go";

In this case, 'scene can be a variant but should be the same for both functions. What can I do to achieve this? What would be idiomatic?

The best I can think of would be to use a module function, whose argument provides the underlying type. For example:

module MakeScene = (Specification: {type scenes}) => {
  type t
  external make: unit => t = "aFunction"
  @send
  external scene: (t, Specification.scenes, 'a => unit) => unit = "scene";

  @send
  external go: (t, Specification.scenes, ~data: 'a=?) => unit = "go";
}

module Spec1 = {
  type scenes = | One | Two
}
module Scene1 = MakeScene(Spec1)
let scene1 = Scene1.make()
Scene1.scene(scene1, Spec1.One, val => ())
Scene1.go(scene1, Spec1.One)

module Spec2 = {
  type scenes = | A | B
}
module Scene2 = MakeScene(Spec2)
let scene2 = Scene2.make()
Scene2.scene(scene2, Spec2.A, val => ())
Scene2.go(scene2, Spec2.A)

Overuse of module functions can over-complicate code, so do be judicious here.

2 Likes

Thanks, was thinking along those lines as well.
One issue with that approach is that I can’t really restrict the Specification to take in a variant that will produce a string. That is another requirement to ensure the runtime behaviour will match.
I suppose that can’t done.

You could use a type t = private string, this way you’d make sure the parameter would be of the right type.

= private string wow, never seen this syntax. Is it documented somewhere?

How would that work?
Doesn’t seem to capture this error: ReScript Playground

something like that:

module MakeScene = (
  Specification: {
    type scenes = private string
  },
) => {
  external scene: (Specification.scenes, 'a => unit) => unit = "scene"

  external go: (Specification.scenes, ~data: 'a=?) => unit = "go"
}

module Spec1: {
  type scenes = private string
  let one: scenes
  let two: scenes
} = {
  type scenes = string
  let one = "one"
  let two = "two"
}

module Scene1 = MakeScene(Spec1)

Scene1.scene(Spec1.one, _val => ())
Scene1.go("other") // compiler error!

Playground link

1 Like