Skip to content

Instantly share code, notes, and snippets.

@serras
Last active December 9, 2020 20:24
Show Gist options
  • Save serras/45ce4737db96e800de298f61955828d5 to your computer and use it in GitHub Desktop.
Save serras/45ce4737db96e800de298f61955828d5 to your computer and use it in GitHub Desktop.

Variants

Author

  • Alejando Serrano (47 Degrees)

Champions

  • Searching for one ;)

Overview

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.

Examples

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:
    ...
}

Syntax

Construction

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

Check tag

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:
}

Spreading

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 }

Destructuring

Variants support two forms of destructuring.

  1. You can destructure using object syntax, which means that the tag is ignored:

    const { name, age } = p
  2. 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

Standard library support

Every variant can be treated as a record unless otherwise specified. They can be iterated, their entries can be obtained, and so forth.

Equality

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

Variant.tag(v: Variant): String

Obtains the tag associated to the variant.

FAQ

Why not simply use a record with a type field?

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, or tag, or __typename.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment