Skip to content

Commit 90576dc

Browse files
committed
Address feedback, add some extra content
Content: - recursive types - JS output for relevant snippets - Variant vs polymorphic variant section
1 parent 9679708 commit 90576dc

File tree

1 file changed

+213
-33
lines changed

1 file changed

+213
-33
lines changed

pages/docs/manual/latest/polymorphic-variant.mdx

Lines changed: 213 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,39 @@ Now that we know what [variant types](./variant) are, let's dive into a more spe
1010

1111
First off, here are some key features:
1212

13-
- Poly variants are **structurally typed** (in comparison to **nominally typed** variants). They can be used without an explicit type definition.
13+
- Poly variants are structurally typed, which means they don't require any explicit type definition to be used as a value, and are not coupled to any specific module. The compiler will infer the type on demand, and compare poly variants by their value, instead of their type name (which is called nominal typing).
1414
- 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.
15+
- Due to their structural nature, poly variant types may cause tricky type checking errors when types don't match up.
1616

1717
## Basics
1818

19-
This is how you'd define a poly variant type with an exact set of constructors:
19+
Here is how you'd construct a poly variant value:
20+
21+
<CodeTab labels={["ReScript", "JS Output"]}>
2022

2123
```res
22-
// Note the surrounding square brackets, and # for constructors
23-
type color = [ #Red | #Green | #Blue ]
24+
// Note how a poly variant starts with a hashtag (#)
25+
// We also don't need any explicit type definition
26+
let myColor = #Red
2427
```
2528

26-
Here is how you'd construct a poly variant value:
29+
```js
30+
var myColor = "Red";
31+
```
32+
33+
</CodeTab>
34+
35+
This is how you'd define a closed poly variant type with an exact set of constructors:
2736

2837
```res
29-
// This doesn't actually need any color type definition
30-
// beforehand
31-
let myColor = #Red
38+
// Note the surrounding square brackets, and # for constructors
39+
type color = [ #Red | #Green | #Blue ]
3240
```
3341

3442
We can also use poly variant types in annotations without an explicit type definition:
3543

44+
<CodeTab labels={["ReScript", "JS Output"]}>
45+
3646
```res
3747
let render = (color: [#Red | #Green | #Blue]) => {
3848
switch(color) {
@@ -43,27 +53,57 @@ let render = (color: [#Red | #Green | #Blue]) => {
4353
let color: [#Red] = #Red
4454
```
4555

56+
```js
57+
function render(color) {
58+
console.log("...");
59+
}
60+
61+
var color = "Red";
62+
```
63+
64+
</CodeTab>
65+
4666
### Constructor Names
4767

4868
Poly variant constructor names are less restrictive than in common variants (e.g. they don't need to be capitalized):
4969

70+
<CodeTab labels={["ReScript", "JS Output"]}>
71+
5072
```res
5173
type users = [ #admin | #moderator | #user ]
5274
5375
let admin = #admin
5476
```
5577

78+
```js
79+
var admin = "admin";
80+
```
81+
82+
</CodeTab>
83+
5684
In rare cases (mostly for JS interop reasons), it's also possible to define invalid identifiers, such as hypens or numbers:
5785

86+
<CodeTab labels={["ReScript", "JS Output"]}>
87+
5888
```res
5989
type numbers = [#\"1" | #\"2"]
6090
let one = #\"1"
91+
let oneA = #\"1a"
92+
```
93+
94+
```js
95+
var one = "1";
96+
var oneA = "1a";
6197
```
6298

99+
</CodeTab>
100+
63101
### Constructor Arguments
64102

65103
This is equivalent to what we've already learned with common variants:
66104

105+
<CodeTab labels={["ReScript", "JS Output"]}>
106+
67107
```res
68108
type account = [
69109
| #Anonymous
@@ -74,29 +114,127 @@ type account = [
74114
let acc: account = #Instagram("test")
75115
```
76116

77-
### Annotations with Upper / Lower Bound Constraints
117+
```js
118+
var acc = {
119+
NAME: "Instagram",
120+
VAL: "test"
121+
};
122+
```
123+
124+
</CodeTab>
125+
126+
### Compose and Pattern Match Poly Variants
127+
128+
You can use poly variant types within other poly variant types to create a sum of all constructors:
129+
130+
<CodeTab labels={["ReScript", "JS Output"]}>
131+
132+
```res
133+
type red = [#Ruby | #Redwood | #Rust]
134+
type blue = [#Sapphire | #Neon | #Navy]
135+
136+
// Contains all constructors of red and blue.
137+
// Also adds #Papayawhip
138+
type color = [red | blue | #Papayawhip]
139+
140+
let c: color = #Ruby
141+
```
142+
143+
```js
144+
var c = "Ruby";
145+
```
78146

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:
147+
</CodeTab>
148+
149+
There's also some special [pattern matching](./pattern-matching-destructuring) syntax to match on constructors defined in a specific poly variant type:
150+
151+
<CodeTab labels={["ReScript", "JS Output"]}>
80152

81153
```res
82-
// Only #Red allowed, no upper / lower bound (= exact)
154+
// Continuing the previous example above...
155+
156+
switch #Papayawhip {
157+
| #...blue => Js.log("This is a blue color")
158+
| #...red => Js.log("This is a red color")
159+
| other => Js.log2("Other color than red and blue: ", other)
160+
}
161+
```
162+
163+
```js
164+
// This code got heavily optimized due to the usage of
165+
// constant values in a switch expression
166+
console.log("Other color than red and blue: ", "Papayawhip");
167+
168+
var c = "Ruby";
169+
```
170+
171+
</CodeTab>
172+
173+
The `switch` expression above is a shorter and more convenient version of:
174+
175+
```res
176+
switch #Papayawhip {
177+
| #Sapphire | #Neon | #Navy => Js.log("This is a blue color")
178+
| #Ruby | #Redwood | #Rust => Js.log("This is a red color")
179+
| other => Js.log2("Other color than red and blue: ", other)
180+
}
181+
```
182+
183+
### Recursive Type Definitions
184+
185+
Poly variant types are non-recursive by default. Use the `rec` keyword to allow recursion:
186+
187+
<CodeTab labels={["ReScript", "JS Output"]}>
188+
189+
```res
190+
type rec markdown = [
191+
| #Text(string)
192+
| #Paragraph(markdown)
193+
| #Ul(array<markdown>)
194+
]
195+
196+
let content: markdown = #Paragraph(#Text("hello world"))
197+
```
198+
199+
```js
200+
var content = {
201+
NAME: "Paragraph",
202+
VAL: {
203+
NAME: "Text",
204+
VAL: "hello world"
205+
}
206+
};
207+
```
208+
209+
</CodeTab>
210+
211+
### Annotations with Closed / Upper / Lower Bound Constraints
212+
213+
There's also a way to define an "upper" and "lower" bound constraint for a poly variant type. Here is what it looks like in a type annotation:
214+
215+
```res
216+
// Only #Red allowed, no upper / lower bound (closed poly variant)
83217
let basic: [#Red] = #Red
84218
85-
// May contain #Red, or any other value (open variant)
86-
// here, foreground will be an inferred type [> #Red | #Green]
219+
// May contain #Red, or any other value (open poly variant)
220+
// here, foreground will actually be inferred as [> #Red | #Green]
87221
let foreground: [> #Red] = #Green
88222
89223
// The value must be "one of" #Red | #Blue
90224
// Only #Red and #Blue are valid values
91225
let background: [< #Red | #Blue] = #Red
92226
```
93227

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).
228+
Don't worry about the upper / lower bound feature 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).
95229

96230

97231
## Polymorphic Variants are Structurally Typed
98232

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.
233+
As we've already seen in the section above, poly variants don't need any explicit type definition to be used as a value.
234+
235+
The compiler treats every value as an independent type and doesn't couple it to any particular module (like with common variants). It therefore compares different poly variant types by their structure, not by a defined type name.
236+
237+
Here is what the type checker sees whenever you are using a poly variant:
100238

101239
```res
102240
// inferred as [> #Red]
@@ -105,7 +243,7 @@ let color = #Red
105243

106244
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.
107245

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:
246+
You can interchangably use variant values from different modules and types as long as a value is part of a constructor set. For example:
109247

110248
```res
111249
type rgb = [#Red | #Green | #Blue]
@@ -121,19 +259,22 @@ let other = [#Green]
121259
let all = Belt.Array.concat(colors, other)
122260
```
123261

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.
262+
As you can see in the example above, the type checker doesn't really care about the fact that `color` is not annotated as an `array<rgb>` type.
125263

126-
**That means that it is very easy to get confusing type errors on the wrong locations!**
264+
As soon as it hits the first constraint (`Belt.Array.concat`), it will try to check if the structural types of `colors` and `other` unify into one poly variant type. If there's a mismatch, you will get an error on the `Belt.Array.concat` call.
127265

128-
For instance, if I'd make a typo like this:
266+
**Be aware that this behavior may cause confusing type errors in the wrong source code locations!**
267+
268+
For instance, if we'd make a typo like this:
129269

130270
```res
271+
// Note the typo in the #Green constructor
131272
let other = [#GreeN]
132273
133274
let all = Belt.Array.concat(colors, other)
134275
```
135276

136-
I'd get an error on the `concat` call, even thought the error was actually caused by the typo in the value assignment of `other`.
277+
We'd get an error on the `concat` call, even thought the error was actually caused by the typo in the value assignment of `other`.
137278

138279
## JavaScript Output
139280

@@ -174,7 +315,39 @@ var num = "1";
174315
</CodeTab>
175316

176317

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.
318+
### Bind to JS Functions
319+
320+
Poly variants play an important role for binding to functions in JavaScript.
321+
322+
For example, let's assume we want to bind to `Intl.NumberFormat` and want to make sure that our users only pass valid locales, we could define an external binding like this:
323+
324+
```res
325+
// IntlNumberFormat.res
326+
type t
327+
328+
@bs.val
329+
external make: ([#\"de-DE" | #\"en-GB" | #\"en-US" ]) => t = "Intl.NumberFormat"
330+
```
331+
332+
We could later use our newly created bindings like this:
333+
334+
<CodeTab labels={["ReScript", "JS Output"]}>
335+
336+
```res
337+
// MyApp.res
338+
339+
let intl = IntlNumberFormat.make(#\"de-DE")
340+
```
341+
342+
```js
343+
var intl = Intl.NumberFormat("de-DE");
344+
```
345+
346+
</CodeTab>
347+
348+
The JS Output is practically identical to handwritten JS, but we also get to enjoy all the benefits of a variant.
349+
350+
More usage examples for poly variant interop can be found in [Bind to JS Function](bind-to-js-function#constrain-arguments-better) and [Generate Converters and Helper](generate-converters-accessors#generate-converters-for-js-string-enums-and-polymorphic-variants).
178351

179352

180353
### Bind to String Enums
@@ -193,7 +366,7 @@ enum Direction {
193366
export const myDirection = Direction.Up
194367
```
195368

196-
For this particular example, we can use poly variants to design the type for the imported `myDirection` value:
369+
For this particular example, we can also inline poly variant type definitions to design the type for the imported `myDirection` value:
197370

198371

199372
<CodeTab labels={["ReScript", "JS Output"]}>
@@ -211,15 +384,15 @@ var myDirection = DirectionJs.myDirection;
211384

212385
</CodeTab>
213386

214-
Since we were using poly variants, the JS Output is practically zero-cost and doesn't add any extra code!
387+
Again: since we were using poly variants, the JS Output is practically zero-cost and doesn't add any extra code!
215388

216389
## Lower / Upper Bound Constraints
217390

218391
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.
219392

220393
**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.
221394

222-
### Exact (`[`)
395+
### Closed (`[`)
223396

224397
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.
225398

@@ -240,8 +413,8 @@ A lower bound defines the minimum set of constructors a poly variant type is awa
240413
Here is an example on how to make a minimum set of `basicBlueTones` extensible for a new `colors` type:
241414

242415
```res
243-
type basicBlueTone<'a> = [> #Blue | #DeepBlue | #Azuro ] as 'a
244-
type color = basicBlueTone<[#Blue | #DeepBlue | #Azuro | #Purple]>
416+
type basicBlueTone<'a> = [> #Blue | #DeepBlue | #LightBlue ] as 'a
417+
type color = basicBlueTone<[#Blue | #DeepBlue | #LightBlue | #Purple]>
245418
246419
let color: color = #Purple
247420
@@ -250,11 +423,13 @@ let color: color = #Purple
250423
type notWorking = basicBlueTone<[#Purple]>
251424
```
252425

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>`.
426+
Here, the compiler will enforce the user to define `#Blue | #DeepBlue | #LightBlue` as the minimum set of constructors when trying to extend `basicBlueTone<'a>`.
427+
428+
**Note:** Since we want to define an extensible poly variant, we need to provide a type placeholder `<'a>`, and also add `as 'a` after the poly variant declaration, which essentially means: "Given type `'a` is constraint to the minimum set of constructors (`#Blue | #DeepBlue | #LightBlue`) defined in `basicBlueTone`".
254429

255430
### Upper Bound (`[<`)
256431

257-
The upper bound works in the exact opposite way: The extending type may only use constructors that are stated in the lower bound constraint.
432+
The upper bound works in the opposite way than a lower bound: the extending type may only use constructors that are stated in the upper bound constraint.
258433

259434
Here another example, but with red colors:
260435

@@ -266,8 +441,13 @@ type myReds = validRed<[#Ash]>
266441
type notWorking = validRed<[#Purple]>
267442
```
268443

269-
## Tips & Tricks
444+
## Variant vs Polymorphic Variant
445+
446+
One might think that polymorphic variants are fastly superior to common [variants](./variant). As always, it depends on the use case:
447+
448+
- Variants allows better encapsulation for your APIs, since they require you to define a type definition that is coupled to a specific module.
449+
- Variants are conceptionally easier to understand, makes your code easy to refactor and provides better exhaustive pattern matching support
450+
- Variants usually deliver better type error messages, especially in recursive type definitions
451+
- Poly variants are useful for expressing strings in JS, and allow different type composition strategies. They can also be defined adhocly in your type definitions.
270452

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
453+
In most scenarios, we'd recommend to use common variants over polymorphic variants, especially when you are writing plain ReScript code. In case you want to write zero-cost interop bindings, poly variants are a better option.

0 commit comments

Comments
 (0)