Skip to content

Commit 8875300

Browse files
committed
Add polyvar docs (latest)
1 parent 10dff18 commit 8875300

File tree

2 files changed

+274
-0
lines changed

2 files changed

+274
-0
lines changed

data/sidebar_manual_latest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"record",
1616
"object",
1717
"variant",
18+
"polymorphic-variant",
1819
"null-undefined-option",
1920
"array-and-list",
2021
"function",
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
---
2+
title: "Polymorphic Variant"
3+
description: "The Polymorphic Variant data structure in ReScript"
4+
canonical: "/docs/manual/latest/polymorphic-variant"
5+
---
6+
7+
# Polymorphic Variant
8+
9+
Now that we know what [variant types](./variant) are, let's dive into a more specific version, so called polymorphic variants (or poly variants).
10+
11+
First off, here are some key features:
12+
13+
- Poly variants are **structurally typed** (in comparison to **nominally typed** variants). They can be used without an explicit type definition.
14+
- They allow easier JavaScript interop (compile to strings / objects with predictable `NAME` and `VAL` attribute) and don't need explicit runtime conversions, unlike common variants.
15+
- Due their structural nature, they oftentimes cause tricky type checking errors when types don't match up, which makes them a more advanced feature.
16+
17+
## Basics
18+
19+
This is how you'd define a poly variant type with an exact set of constructors:
20+
21+
```res
22+
// Note the surrounding square brackets, and # for constructors
23+
type color = [ #Red | #Green | #Blue ]
24+
```
25+
26+
Here is how you'd construct a poly variant value:
27+
28+
```res
29+
// This doesn't actually need any color type definition
30+
// beforehand
31+
let myColor = #Red
32+
```
33+
34+
We can also use poly variant types in annotations without an explicit type definition:
35+
36+
```res
37+
let render = (color: [#Red | #Green | #Blue]) => {
38+
switch(color) {
39+
| _ => Js.log("...")
40+
}
41+
}
42+
43+
let color: [#Red] = #Red
44+
```
45+
46+
### Constructor Names
47+
48+
Poly variant constructor names are less restrictive than in common variants (e.g. they don't need to be capitalized):
49+
50+
```res
51+
type users = [ #admin | #moderator | #user ]
52+
53+
let admin = #admin
54+
```
55+
56+
In rare cases (mostly for JS interop reasons), it's also possible to define invalid identifiers, such as hypens or numbers:
57+
58+
```res
59+
type numbers = [#\"1" | #\"2"]
60+
let one = #\"1"
61+
```
62+
63+
### Constructor Arguments
64+
65+
This is equivalent to what we've already learned with common variants:
66+
67+
```res
68+
type account = [
69+
| #Anonymous
70+
| #Instagram(string)
71+
| #Facebook(string, int)
72+
]
73+
74+
let acc: account = #Instagram("test")
75+
```
76+
77+
### Annotations with Upper / Lower Bound Constraints
78+
79+
There's also a way to define an "upper" and "lower" bound constraint for a poly variant type (that's why they are called _Polymorphic Variants_). Here is what it looks like in type annotations:
80+
81+
```res
82+
// Only #Red allowed, no upper / lower bound (= exact)
83+
let basic: [#Red] = #Red
84+
85+
// May contain #Red, or any other value (open variant)
86+
// here, foreground will be an inferred type [> #Red | #Green]
87+
let foreground: [> #Red] = #Green
88+
89+
// The value must be "one of" #Red | #Blue
90+
// Only #Red and #Blue are valid values
91+
let background: [< #Red | #Blue] = #Red
92+
```
93+
94+
Don't worry about the upper / lower bound feature (aka polymorphism) just yet, since this is a very advanced topic that's often not really needed. For the sake of completeness, we mention a few details about it [later on](#lower--upper-bound-constraints).
95+
96+
97+
## Polymorphic Variants are Structurally Typed
98+
99+
As we've already seen in the section above, poly variants are treated a little bit differently than common variants. Most notably, we don't need any explicit type definition to define a value.
100+
101+
```res
102+
// inferred as [> #Red]
103+
let color = #Red
104+
```
105+
106+
The compiler will automatically infer the `color` binding as a value of type `[> #Red]`, which means `color` will type check with any other poly variant type that defines `#Red` in its constructors.
107+
108+
This means that you can essentially mix and match poly variant values from different sources, as long as all constructors are defined in the final interface. For example:
109+
110+
```res
111+
type rgb = [#Red | #Green | #Blue]
112+
113+
let colors: array<rgb> = [#Red]
114+
115+
// `other` is inferred as a type of array<[> #Green]>
116+
let other = [#Green]
117+
118+
// Because `other` is of type `array<[> Green]>`,
119+
// this will type check even though we didn't define
120+
// `other`to be of type rgb
121+
let all = Belt.Array.concat(colors, other)
122+
```
123+
124+
As you can see in the example above, the type checker doesn't really care that `color` is not annotated as an `array<rgb>` type. As soon as it hits the first constraint (`Belt.Array.concat`), it will try to check if `colors` and `other` unify into one polymorphic type. If there's a mismatch, you will get an error on the `Belt.Array.concat` call.
125+
126+
**That means that it is very easy to get confusing type errors on the wrong locations!**
127+
128+
For instance, if I'd make a typo like this:
129+
130+
```res
131+
let other = [#GreeN]
132+
133+
let all = Belt.Array.concat(colors, other)
134+
```
135+
136+
I'd get an error on the `concat` call, even thought he error actually stems from my typo in my `other` definition.
137+
138+
## JavaScript Output
139+
140+
Poly variants are a [shared data structure](./shared-data-types), so they are very useful to bind to JavaScript. It is safe to rely on its compiled JS structure.
141+
142+
A value compiles to the following JavaScript output:
143+
144+
- If the variant value is a constructor without any payload, it compiles to a string of the same name
145+
- Values with a payload get compiled to an object with a `NAME` attribute stating the name of the constructor, and a `VAL` attribute containing the JS representation of the payload.
146+
147+
Check the output in these examples:
148+
149+
150+
<CodeTab labels={["ReScript", "JS Output"]}>
151+
152+
```res example
153+
let capitalized = #Hello
154+
let lowercased = #goodbye
155+
156+
let err = #error("oops!")
157+
158+
let num = #\"1"
159+
```
160+
161+
```js
162+
var capitalized = "Hello";
163+
164+
var lowercased = "goodbye";
165+
166+
var err = {
167+
NAME: "error",
168+
VAL: "oops!"
169+
};
170+
171+
var num = "1";
172+
```
173+
174+
</CodeTab>
175+
176+
177+
**Note:** Poly variants play an important role for binding to JS functions in existing JavaScript. Check out the [Bind to JS Function page](bind-to-js-function#constrain-arguments-better) to learn more.
178+
179+
180+
### Bind to String Enums
181+
182+
Let's assume we have a TypeScript module that expresses following (stringly typed) enum export:
183+
184+
```js
185+
// direction.js
186+
enum Direction {
187+
Up = "UP",
188+
Down = "DOWN",
189+
Left = "LEFT",
190+
Right = "RIGHT",
191+
}
192+
193+
export const myDirection = Direction.Up
194+
```
195+
196+
For this particular example, we can use poly variants to design the type for the imported `myDirection` value:
197+
198+
199+
<CodeTab labels={["ReScript", "JS Output"]}>
200+
201+
```res
202+
type direction = [ #UP | #DOWN | #LEFT | #RIGHT ]
203+
@bs.module("./direction.js") external myDirection: direction = "myDirection"
204+
```
205+
206+
```js
207+
var DirectionJs = require("./direction.js");
208+
209+
var myDirection = DirectionJs.myDirection;
210+
```
211+
212+
</CodeTab>
213+
214+
Since we were using poly variants, the JS Output is practically zero-cost and doesn't add any extra code!
215+
216+
## Lower / Upper Bound Constraints
217+
218+
There are a few different ways to define constraints on a poly variant type, such as `[>`, `[<` and `[]`. Some of them were briefly mentioned before, so in this section we will quickly explain what this syntax is about.
219+
220+
**Note:** We added this info for educational purposes. In most cases you will not want to use any of this stuff, since it makes your APIs pretty unreadable / hard to use.
221+
222+
### Exact (`[`)
223+
224+
This is the simplest poly variant definition, and also the most practical one. Like a common variant type, this one defines an exact set of constructors.
225+
226+
```res
227+
type rgb = [ #Red | #Green | #Blue ]
228+
229+
let color: rgb = #Green
230+
```
231+
232+
In the example above, `color` will only allow one of the three constructors that are defined in the `rgb` type. This is usually the way how poly variants should be defined.
233+
234+
In case you want to define a type that is extensible in polymorphic ways (or in other words, subtyping allowed sets of constructors), you'll need to use the lower / upper bound syntax.
235+
236+
### Lower Bound (`[>`)
237+
238+
A lower bound defines the minimum set of constructors a poly variant type is aware of. It is also considered an "open poly variant type", because it doesn't restrict any additional values.
239+
240+
Here is an example on how to make a minimum set of `basicBlueTones` extensible in a new `colors` type:
241+
242+
```res
243+
type basicBlueTone<'a> = [> #Blue | #DeepBlue | #Azuro ] as 'a
244+
type color = basicBlueTone<[#Blue | #DeepBlue | #Azuro | #Purple]>
245+
246+
let color: color = #Purple
247+
248+
249+
// This will fail due to missing minimum constructors:
250+
type notWorking = basicBlueTone<[#Purple]>
251+
```
252+
253+
Here, the compiler will enforce the user to define `#Blue | #DeepBlue | #Azuro` as the minimum set of constructors when trying to extend `basicBlueTone<'a>`.
254+
255+
### Upper Bound (`[<`)
256+
257+
The upper bound works in the exact opposite way: The extending type may only use constructors that are stated in the lower bound constraing.
258+
259+
Same example:
260+
261+
```res
262+
type validRed<'a> = [< #Fire | #Crimson | #Ash] as 'a
263+
type myReds = validRed<[#Ash]>
264+
265+
// This will fail due to unlisted constructor not defined by the lower bound
266+
type notWorking = validRed<[#Purple]>
267+
```
268+
269+
## Tips & Tricks
270+
271+
- In most scenarios, you should prefer common variants over polymorphic variants, since they offer better error messages and easier to spot errors in your program.
272+
- Polymorphic variants are pretty useful for doing zero-cost interop, e.g. when binding to JavaScript string enums, or to bind seemlessly between a tagged union type in TypeScript and ReScript
273+
- Even though we expanded a little bit on the upper / lower bounds / polymorphism, these examples only

0 commit comments

Comments
 (0)