Introduction

Polymorphic variants in Rescript are strange beasts. They aren’t generally that useful in pure Rescript programming, but are frequently necessary in binding to Javascript.

Polymorphic variants are extremely flexible (too flexible), but one of the most common uses is to represent a list of constant strings that a JS binding can accept. For example, the ubiquitous on function used for event handlers typically takes a string as its first parameter, as in on("click"...). In Rescript, you can prevent typoing that string using Polymorphic variants: on(#click).

Patreon

This article is part of a series on Rescript programming, although unlike its predecessors, it is a standalone article rather than building on previous work.

This series takes a lot of time and dedication to write and maintain. The main thing that has kept me invested in this writing this series is support from my Patrons.

Other ways to show support include sharing the articles on social media, commenting on them here, or a quick thank you on the Rescript forum.

With over a dozen articles and counting, I’ve created a table of contents listing all the articles in this series in reading order.

Table of Contents

The Motivating Problem

While writing bindings to the excellent Dexie IndexedDB wrapper, I found I needed a generic type that required a generic set of strings. A consumer of the API would be able to specify what those strings are for their program, and Rescript needed to somehow enforce that any use of those strings would always be the same.

In the case of Dexie, I have several functions that accept a table name as an argument. Any given user will require completely different table names, but once that set of names is defined, they’re never going to want to use a name from outside that set.

Rescript doesn’t have anything like Typescript’s generic functions, unless you wrap them in functors, which I’ll discuss as another way to solve this in a later article.

That’s where polymorphic variants come in. The docs on polymorphic variants, especially the “constraints” portion are rather vague and don’t have much in the way of real-world examples. I had a real-world problem now, and the docs failed to help me solve it.

Luckily trial and compiler-error was enough to solve it for me. Hopefully by writing it up, Google will be able to tell you to come here instead of wading through quite as many compiler errors.

The Goal

We want to be able to write code that looks like this:

type database<'a> = ???????


let log: (database<'a>, ???????) => 'a = (db, value) => {
  Js.log2(db.name, value)
  value
}

in a generic fashion such that the log function will throw a compile-time error if you pass a polymorphic variant that has not been specified.

For example, a database of colours might be set up like this:

type coloursDb = database<[#Blue | #Red | #Coral]>

let colours: coloursDb = {
  name: "Colours",
}

While a database of dogs would be set up like this:

type dogsDb = database<[#Corgi | #"King Charles Spaniel" | #Beagle]>

let dogs: dogsDb = {
  name: "Dogs",
}

The goal, then is to be able to call log passing in the colours object by passing in a valid colour, and passing the dogs object with a valid dog, but to have a type error if you try to pass a colour into the dogs db:

let m = log(colours, #Blue) // is legal and
let m = log(dogs, #Corgi) // is legal but
let m = log(dogs, #Blue) // is compile-time error

The solution (spoiler alert)

I hate the phrase tl;dr, but here it is:

type poly<'a> = [> ] as 'a

type databaseGeneric<'a> = {name: string}
type database<'a> = databaseGeneric<poly<'a>>

type coloursDb = database<[#Blue | #Red | #Coral]>
type dogsDb = database<[#Corgi | #"King Charles Spaniel" | #Beagle]>

If you’re not sure how that could possibly work, let’s break it down.

Breaking it down

Obviously the first thing we need is a database type:

type database

That database can contain different values depending on whether we are tracking dogs or colours, so it obviously needs to be generic:

type databaseGeneric<'a>

And to keep things concrete, let’s also give the generic an additional field (I’m including this because it was tripping me up and the Rescript docs were no help):

type databaseGeneric<'a> = {name: string}

Unfortunately, this database is too generic! Our database is supposed to be restricted to poly variant, but databaseGeneric currently allows any generic type. For example:

type databaseInt = database<int>

Shouldn’t be legal, but as we’ve currently set it up,

type databaseInt = databaseGeneric<int> is valid.

We can probably solve this by creating a new type that restricts 'a to polymorphic variant types, but what’s the syntax?

The problem is, polyvariant isn’t a type.

Constraints

The Rescript docs on polymorphic variants have a section on extra constraints, but they explicitly tell us “In most cases you will not want to use any of this stuff, since it makes your APIs pretty unreadable / hard to use.”

However, I think this is a valid use case that will make my API more readable and easier to use, so I’m going to take it.

The key is that it is possible to define poly variant types with lower or upper bounds. A lower bound variant looks like this (directly from the docs):

type basicBlueTone<'a> = [> #Blue | #DeepBlue | #LightBlue ] as 'a

Don’t ask me what the as syntax means, precisely; I can’t find any documentation on it and my question on the rescript forum has not been answered yet.

The > at the beginning defines a lower bound constraint on the poly variant. That means that a basicBlueTone can take on the values #Blue, #DeepBlue, or #LightBlue or any other poly variant value. You can therefore make a concrete type from this generic that looks like this:

type color = basicBlueTone<[#Blue | #DeepBlue | #LightBlue | #Purple]>

but not one that looks like this, because it doesn’t include the minimally required three blue values:

type notWorking = basicBlueTone<[#Purple]>

I won’t go into the upper bound constraint, indicated by a [< ...] but it’s basically the inverse: You can specify a generic polymorphic variant type for which any specializations must be some subset of those types.

The poly variant specialization

I said earlier that polyvariant isn`t a type. However, the lower bound constraint on a variant allows us to define a poly variant type that can take on any possible polymorphic variant type:

type poly<'a> = [> ] as 'a

This is saying that poly represents a generic type where the lower bound on any specialization of that type is the set of no polymorphic variants.

In more readable English, any set of poly variants is a valid poly specialization. Specifically, poly<[#Blue | #Red | #Coral]> and poly<[#Corgi | #"King Charles Spaniel" | #Beagle]> are both valid specializations of poly<'a>, but poly<int> is not.

Which is exactly what we were looking for! Now we can define a databaseGeneric specialization that only accepts poly variants:

type database<'a> = databaseGeneric<poly<'a>>

With that type in place, the log function can be defined as follows to get the desired behaviour:

let log: (database<'a>, 'a) => 'a = (db, value) => {
  Js.log2(db.name, value)
  value
}

The generic 'a is used in three places here. Contrary to normal rules of English and coding, let’s read them from right to left. The return value 'a can be literally any generic type. Rescript doesn’t care what it is.

However, the second parameter is the same 'a. This forces the second parameter to have the same type as the return value.

The first parameter is a database<'a>. This means that the specialization of the generic database must be the same as the second parameter and the return value. More importantly, all three types (since they are the same type) must also obey any restrictions on the database type, which is to say, 'a must be a poly variant of some sort, and that they must all be from the same poly variant.

Conclusion

I understand polymorphic variants a little better now, and I hope you do, too. The academic discription in the docs was a little too abstract and I didn’t really understand the point of it. Now that the little “aha” moment has clicked for me, I suspect I’ll have more to write on this topic in the future.