Unions

Declaring unions

A union value chooses one of several possible types. These are called the case types.
Below, the case types of u are nat and string. So a u value must be either a nat or a string.
A value of a case type can implicitly convert to the union type.

main void() a u = 1 log a b u = "foo" log b u union nat string to json(a u)

Short unions can be squeezed onto one line.

u union(nat, string)

Case getters

Declaring the union implicitly creates functions for getting each of its case types.
In this example, those are nat64 nat64?(a u) and string string?(a u) .
(nat is an alias for nat64, so the case getter function uses the type's true name.)

main void() a u = 1 log nat64 a log string a b u = "foo" log nat64 b log string b u union(nat, string)

If a case getter is used as a condition, its argument will implicitly convert to the case type in the scope where the condition is true.

main void() a u = 1 b u = "foo" log size a log size b u union(nat, string) size nat(a u) if nat64 a a # 'a' implicitly converts to 'nat' elif string a # 'a' implicitly converts to 'string', # so this calls the 'size' function for strings size a else # This should never happen 99

"match" on unions

match works on a union value. This uses the same syntax as a match on an enum value.
If you handle every case, you don't need an else branch.

main void() a u = 1 b u = "foo" log size a log size b u union(nat, string) size nat(a u) match a as nat # 'a' implicitly converts to 'nat' a as string # 'a' implicitly converts to 'string' size a

Methods

A union can list functions that must exist for all case types. These are called methods.

main void() a u = 1 b u = "foo" log size a log size b u union nat string do size nat() size nat(a nat) a

For each case type, there must be a function matching the method, with the case type injected as the first parameter.
This is called the method implementation.

Above, the method implementations are size nat(a nat) and size nat(a string) .
The nat implementation is defined in the example; the string implementation comes from the standard library.

Declaring the method also implicitly creates a caller function which takes the union type as its first parameter.
Above, the caller is size nat(a u) .
The caller matches on the union and calls the appropriate implementation.

If the union is on one line, omit the do keyword.

u union(nat[], string) size nat()

Auto functions on unions

Unions support the same auto functions as records.

main void() n u = 10 s u = "9" # Union cases compare in declaration order log n <=> s u union(nat, string) == bool(a u, b u) <=> comparison(a u, b u) hash void(a u, state hash-state) to json(a u)
  • == returns false for different case types. For values of the same case type, it calls the == function for that type.
  • <=> works similarly. When the values are of the same case type, it calls <=> for the case type.
    When the values are not of the same case type, case types that were declared earlier are considered lesser.
    If you declared the union as union(nat, string) , nats are less than strings.
    If you declared the union as union(string, nat) , strings are less than nats.
  • hash hashes the case type index (above nat is 0 and string is 1), then calls the hash function for the case type.
  • to json returns a single-key object with the case type name as a key, and calls that type's to json for the value.

Restrictions

Almost any types can be combined in a union, but:

  • Multiple aliases to the same type (e.g. nat and nat64) aren't allowed.
  • Multiple instances of the same generic type aren't allowed. (These will be explained in Type parameters.)
# This won't work u union(nat, nat64) v union(nat[], string[])

You can work around that by defining wrapper types with unique names.

main void() a u = nats (1, 2, 3) log a b u = strings ("a", "b", "c") log b u union(nats, strings) to json(a u) nats record(values nat[]) to json(a nats) strings record(values string[]) to json(a strings)