Skip to content

Latest commit

 

History

History
454 lines (298 loc) · 13.6 KB

polymorphic-variant.mdx

File metadata and controls

454 lines (298 loc) · 13.6 KB
titledescriptioncanonical
Polymorphic Variant
The Polymorphic Variant data structure in ReScript
/docs/manual/latest/polymorphic-variant

Polymorphic Variant

Now that we know what variant types are, let's dive into a more specific version, so called polymorphic variants (or poly variants).

First off, here are some key features:

  • 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 would be called nominal typing).
  • 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.
  • Due to their structural nature, poly variant types may cause tricky type checking errors when types don't match up.

Basics

Here is how you'd construct a poly variant value:

<CodeTab labels={["ReScript", "JS Output"]}>

// Note how a poly variant starts with a hashtag (#)// We also don't need any explicit type definitionletmyColor=#Red
varmyColor="Red";

This is how you'd define a closed poly variant type with an exact set of constructors:

// Note the surrounding square brackets, and # for constructorstypecolor= [ #Red | #Green | #Blue ]

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

<CodeTab labels={["ReScript", "JS Output"]}>

letrender= (color: [#Red | #Green | #Blue]) => { switch(color) { | _=>Js.log("...") } } letcolor: [#Red] =#Red
functionrender(color){console.log("...");}varcolor="Red";

Constructor Names

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

<CodeTab labels={["ReScript", "JS Output"]}>

typeusers= [ #admin | #moderator | #user ] letadmin=#admin
varadmin="admin";

In rare cases (mostly for JS interop reasons), it's also possible to define "invalid identifiers", such as hypens or numbers:

<CodeTab labels={["ReScript", "JS Output"]}>

typenumbers= [#"1" | #"2"] letone=#"1"letoneA=#"1a"
varone="1";varoneA="1a";

Note: For ReScript versions < 9.0 you'll need to use the \ character as an escape sequence to represent invalid identifiers (e.g. #\"1").

Constructor Arguments

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

<CodeTab labels={["ReScript", "JS Output"]}>

typeaccount= [ | #Anonymous | #Instagram(string) | #Facebook(string, int) ] letacc: account=#Instagram("test")
varacc={NAME: "Instagram",VAL: "test"};

Compose and Pattern Match Poly Variants

You can use poly variant types within other poly variant types to create a sum of all constructors:

<CodeTab labels={["ReScript", "JS Output"]}>

typered= [#Ruby | #Redwood | #Rust] typeblue= [#Sapphire | #Neon | #Navy] // Contains all constructors of red and blue.// Also adds #Papayawhiptypecolor= [red | blue | #Papayawhip] letc: color=#Ruby
varc="Ruby";

There's also some special pattern matching syntax to match on constructors defined in a specific poly variant type:

<CodeTab labels={["ReScript", "JS Output"]}>

// Continuing the previous example above...switch#Papayawhip { | #...blue=>Js.log("This is a blue color") | #...red=>Js.log("This is a red color") | other=>Js.log2("Other color than red and blue: ", other) }
// This code got heavily optimized due to the usage of// constant values in a switch expressionconsole.log("Other color than red and blue: ","Papayawhip");varc="Ruby";

The switch expression above is a shorter and more convenient version of:

switch#Papayawhip { | #Sapphire | #Neon | #Navy=>Js.log("This is a blue color") | #Ruby | #Redwood | #Rust=>Js.log("This is a red color") | other=>Js.log2("Other color than red and blue: ", other) }

Recursive Type Definitions

Poly variant types are non-recursive by default. Use the rec keyword to allow recursion:

<CodeTab labels={["ReScript", "JS Output"]}>

typerecmarkdown= [ | #Text(string) | #Paragraph(markdown) | #Ul(array<markdown>) ] letcontent: markdown=#Paragraph(#Text("hello world"))
varcontent={NAME: "Paragraph",VAL: {NAME: "Text",VAL: "hello world"}};

Annotations with Closed / Upper / Lower Bound Constraints

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:

// Only #Red allowed, no upper / lower bound (closed poly variant)letbasic: [#Red] =#Red// May contain #Red, or any other value (open poly variant)// here, foreground will actually be inferred as [> #Red | #Green]letforeground: [> #Red] =#Green// The value must be "one of" #Red | #Blue// Only #Red and #Blue are valid valuesletbackground: [< #Red | #Blue] =#Red

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.

Polymorphic Variants are Structurally Typed

As we've already seen in the section above, poly variants don't need any explicit type definition to be used as a value.

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.

Here is what the type checker sees whenever you are using a poly variant:

// inferred as [> #Red]letcolor=#Red

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.

You can interchangably use variant values from different modules and types as long as a value is part of a constructor set. For example:

typergb= [#Red | #Green | #Blue] letcolors: array<rgb> = [#Red] // `other` is inferred as a type of array<[> #Green]>letother= [#Green] // Because `other` is of type `array<[> Green]>`,// this will type check even though we didn't define// `other`to be of type rgbletall=Belt.Array.concat(colors, other)

As you can see in the example above, the type checker doesn't really care about the fact that other 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 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.

Be aware that this behavior may cause confusing type errors in the wrong source code locations!

For instance, if we'd make a typo like this:

// Note the typo in the #Green constructorletother= [#GreeN] letall=Belt.Array.concat(colors, other)

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.

JavaScript Output

Poly variants are a shared data structure, so they are very useful to bind to JavaScript. It is safe to rely on its compiled JS structure.

A value compiles to the following JavaScript output:

  • If the variant value is a constructor without any payload, it compiles to a string of the same name
  • 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.

Check the output in these examples:

<CodeTab labels={["ReScript", "JS Output"]}>

letcapitalized=#Helloletlowercased=#goodbyeleterr=#error("oops!") letnum=#\"1"
varcapitalized="Hello";varlowercased="goodbye";varerr={NAME: "error",VAL: "oops!"};varnum="1";

Bind to JS Functions

Poly variants play an important role for binding to functions in JavaScript.

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:

// IntlNumberFormat.restypet @bs.valexternalmake: ([#\"de-DE" | #\"en-GB" | #\"en-US" ]) =>t="Intl.NumberFormat"

We could later use our newly created bindings like this:

<CodeTab labels={["ReScript", "JS Output"]}>

// MyApp.resletintl=IntlNumberFormat.make(#\"de-DE")
varintl=Intl.NumberFormat("de-DE");

The JS Output is practically identical to handwritten JS, but we also get to enjoy all the benefits of a variant.

More usage examples for poly variant interop can be found in Bind to JS Function and Generate Converters and Helper.

Bind to String Enums

Let's assume we have a TypeScript module that expresses following (stringly typed) enum export:

// direction.jsenumDirection{ Up ="UP", Down ="DOWN", Left ="LEFT", Right ="RIGHT",}exportconstmyDirection=Direction.Up

For this particular example, we can also inline poly variant type definitions to design the type for the imported myDirection value:

<CodeTab labels={["ReScript", "JS Output"]}>

typedirection= [ #UP | #DOWN | #LEFT | #RIGHT ] @bs.module("./direction.js") externalmyDirection: direction="myDirection"
varDirectionJs=require("./direction.js");varmyDirection=DirectionJs.myDirection;

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

Lower / Upper Bound Constraints

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.

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.

Closed ([)

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.

typergb= [ #Red | #Green | #Blue ] letcolor: rgb=#Green

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.

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.

Lower Bound ([>)

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.

Here is an example on how to make a minimum set of basicBlueTones extensible for a new color type:

typebasicBlueTone<'a> = [> #Blue | #DeepBlue | #LightBlue ] as'atypecolor=basicBlueTone<[#Blue | #DeepBlue | #LightBlue | #Purple]> letcolor: color=#Purple// This will fail due to missing minimum constructors:typenotWorking=basicBlueTone<[#Purple]>

Here, the compiler will enforce the user to define #Blue | #DeepBlue | #LightBlue as the minimum set of constructors when trying to extend basicBlueTone<'a>.

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".

Upper Bound ([<)

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.

Here another example, but with red colors:

typevalidRed<'a> = [< #Fire | #Crimson | #Ash] as'atypemyReds=validRed<[#Ash]> // This will fail due to unlisted constructor not defined by the lower boundtypenotWorking=validRed<[#Purple]>

Variant vs Polymorphic Variant

One might think that polymorphic variants are fastly superior to common variants. As always, it depends on the use case:

  • Variants allow better encapsulation for your APIs, because they always come with a type definition that is coupled to a specific module.
  • Variants are conceptionally easier to understand, makes your code easy to refactor and provides better exhaustive pattern matching support
  • Variants usually deliver better type error messages, especially in recursive type definitions
  • 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.

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 or generate clean JS output, poly variants are oftentimes a better option.

close