Unions#

A union is a value that is one of several types, written with |:

Two shapes of union earn their own names, because they are what other languages reach for a keyword to express: a union of literals is a closed set of values, and a union of records sharing a literal-typed field is a tagged union.

A union of literals is an enum#

A string literal is a type containing exactly that value, so a union of them is a closed set of strings:

Nupp has no enum declaration; this is the spelling. Nothing is declared at run time — the value is the plain string — and a bare literal lands in the union:

A string that is not a member is rejected, and the message says which values were on offer:

Because the value is a string, everything that works on strings works here, and a member is also accepted where a cstring is wanted.

A union may mix a closed set with an open type, which gives up exhaustiveness but keeps the named cases:

Boolean literals are types too. false exists as one on its own so that T | false narrows usefully:

An alias is transparent, so Color and its union are interchangeable. That is also why a diagnostic prints the members rather than the alias: there is no nominal identity behind the name to print instead.

A union of records is a tagged union#

A literal member carries no data. When the alternatives need to, give each record a field whose type is a literal — the tag — and union the records:

Comparing the tag narrows the union to the one record that declares it, so the fields of that arm are reachable and the other arm's are not:

Construction fills the tag like any other field:

The tag is an ordinary field, so it survives to run time and a print of the value shows it. That is the trade against a nominal sum type: the discriminant costs a field, and in exchange the value is a plain table that serializes, compares, and prints without help.

A tag copied into a local is still a tag:

Assigning to shape, or to anything the copy came from, drops what the copy proved. See narrowing for the rest of the rules.

Success and failure#

The arms need share nothing but the tag, so the same shape carries a result and the reason it is not one:

Exhaustiveness#

When a dispatch on a closed set of literals has every branch return, the checker reports the members you left out:

warning: NUPP2107 exhaustiveness: every branch returns, so this handles
"blue" | "green" | "red" and leaves "blue" unhandled
help: add branches for "blue" or add an else clause

Adding the branch or an else clears it. The diagnostic gives help rather than an edit, because it cannot invent the body of the branch you are missing.

This is the exhaustiveness lint, NUPP2107, a correctness lint at warning. A project raises it to error in nupp.lua, and a single deliberate exception writes @allow("exhaustiveness") on the statement.

Exhaustiveness counts single literal types and unions of them. It does not run over a union of records: a dispatch there tests a field rather than the value, and the checker does not count the arms.

Narrowing#

Comparing against a member narrows in both directions:

e is T narrows a union whose members are distinguishable by type rather than by value:

See narrowing for the rest.

Choosing between them#

  • The alternatives are values, and nothing rides along: a union of literals.
  • The alternatives carry different data: a tagged union of records.
  • The alternatives are unrelated existing types, told apart by is: a plain union, no tag needed.