- Alejando Serrano (47 Degrees)
- Searching for one ;)
Many languages, including ReScript, F#, and Elm, have the notion of variant or discriminated union as part of their language. In short, an element of the variant is composed of a tag along with some data. Variants are well-understood, the closest relative to this proposal are polymorphic variants as found in ReasonML / ReScript.
This proposal extends the Records & Tuples one with a tag in front of them. As records and tuples, variants are meant to be deeply immutable.
Variants follow the syntax of records, but instead of simply #
you write the name of a tag in front of it. If the set of fields is empty, the { }
can be removed; that means that we can use variants as enumerations.
// Different tags for different kinds of users
const user1 = #Person {
name: "Alejandro",
age: 32
}
const user2 = #Company {
name: "JS Co",
// This is equivalent to #Netherlands {}
basedOn: #Netherlands
}
// Variant fields are accessed as usual
console.log(user1.name)
console.log(user2.name)
// We can check the case with 'switch'
const greet = (user) => {
switch (user) {
case #Person:
return "hello, " + user.name
case #Company:
return "To the director of " + user.name
}
}
Variants give a clean look to sets of actions and events, as found in Redux. In fact, Elm and Reductive -- whose underlying languages feature unions -- use them to provide Redux-like features.
// A few actions
#AddTodo { text: 'Go to swimming pool' }
#ToggleTodo { index: 1 }
// Matching on those
switch (action) {
case #AddTodo:
...
case #ToggleTodo:
...
}
We define a variant by using #
followed by a tag in front of an object expression:
#Thing { a: 1, b: true }
We can also define the tag dynamically using a string, or taking it from another variant:
const tag = 'Thing'
#[tag] { a: 1, b: true }
Note: this proposal have a potential conflict, since #[tag]
can be interpreted as a tuple. We would disambiguate by checking whether after #[tag]
we find an object expression (then we have a variant) or not (then we have a tuple).
The switch
is extended to allow case #Tag
. Tag
must be a statically-known tag, #[tag]
is not allowed in that context.
switch (expression) {
case #This:
...
case #That:
...
default:
}
Variants support spread syntax. Note that it only copies the fields, not the tag. You can spread on objects or records.
const p = #Person { name: "Alejandro", age: 32 }
const o = { ...p } // [object] { name: "Alejandro", age: 32 }
const r = #{ ...p } // [record] #{ name: "Alejandro", age: 32 }
As explained above, you can copy the tag of a variant v
by using #[v]
. That means that to copy an entire variant you actually need to repeat its name:
const copy = #[p] { ...p }
Variants support two forms of destructuring.
-
You can destructure using object syntax, which means that the tag is ignored:
const { name, age } = p
-
You can destructure using a tag, in which case it's only successful when the tag matches the given one:
const #Person { name, age } = p
We provide no way to obtain the tag of a variant using destructuring. As in the case of regular destructuring, though, you can use dynamic property names and also dynamic tag names:
const tag = 'Person'
const #[tag] { name, age } = p
Every variant can be treated as a record unless otherwise specified. They can be iterated, their entries can be obtained, and so forth.
===
checks for equality of the tags, and equality of each of the fields.
Variant(tag: String, obj: Object): Variant
and Variant.fromEntries(tag: String, iterator: Iterator): Variant
These two methods build a new variant. The interface is a copy of Record
's, but with the addition of a tag.
Obtains the tag associated to the variant.
There are a few advantages to having variants as part of the language:
- The syntax strongly indicates that you are working with data which admits several possibilities, both at creation and at checking time.
- No more discussions about the field being named
type
, ortag
, or__typename
.