Union
Type
Union is a data structure used to hold a value that can take on several different types. Only one of the types can be in use at any one time. The union can be thought of as a type having several cases, each of which can be handled in a different way when the type is manipulated.
Rationale
Unions define pure data. In contrast to protocols, they don't express business logic, nor do they carry identity without being embedded in a protocol.
Unions allow a degree of polymorphism while being type-safe.
Declaration
A union type is declared by the union
keyword, followed by the name (starting with Uppercase), and the list of
possible types between curly brackets {}
. A union parameter, field or variable is declared like any other variable
with the user-defined union type name.
The types inside a union can be any primitive type, struct, identifier, symbol, protocol, or generic type (e.g.
List<Number>
). Unions cannot be nested -- the following is invalid:
union First {
Number,
Text
}
union Second {
Boolean,
First // First is a union, cannot be a subtype itself
}
Initialization
A union literal starts with the type name, followed by the value initialization between parentheses, similar to a function call.
Given
union Integral {
Number, Boolean, Text
};
then
var u = Integral(47);
u = Integral(true);
Values can be automatically converted to a union type in an assignment or a function call without explicitly mentioning their name, e.g.
function useIntegral(i: Integral) -> {
// ....
}
function test() -> {
var u = 47; // automatic conversion from Number to union
useIntegral(true); // automatic conversion from Boolean to union
}
Note that the opposite direction conversion (from union to actual type) does not happen automatically.
Given
struct S1 {
x: Number
}
union U1 {
S1, Number
}
then the following is invalid:
function test(u: U) returns Number {
return u.x; // unresolved reference to x
}
Match statements and expressions
To determine the type of the value that a union actually holds, a
match
statement or expression is used.
Matches on unions use is
-expressions that always specify a concrete type. This type is then associated with an
expression or statement that is evaluated when the union holds a value of that type. An else
branch is executed if
none of the is
-branches match.
Within the associated expression, the compiler deduces the actual type of the union, and allows it to be used as such. This feature is known as "smart casting". For instance, the following is valid:
symbol eur;
struct Data { x: Number }
union Union { Data, Number, eur }
function double(x: Number) returns Number -> { return 2 * x; }
function useUnion(u: Union) returns Number -> {
var res = 3;
match (u) {
is Data -> { res = u.x; } // ok -> u is known to be S
is Number -> { res = double(u); } // ok -> u is known to be a Number
else -> { res = u.toNumber(); } // ok -> u can only be the symbol chf, everything else is matched
};
return res;
}
Or when used as an expression:
function returnUnion(u: Union) returns Number -> {
return match (u) {
is Data -> u.x
is Number -> double(u)
else -> u.toNumber()
};
}
Smart casting is facilitated by control flow analysis, which tracks the variable type at every position of the program. The following is valid:
function flow(u: Union) returns Number -> {
match (u) {
is Data -> { return u.x; } // Data is matched
};
return match (u) { // exhaustive, because 'u' cannot be Data at this point
is Number -> u
is eur -> u.toNumber()
};
}
Union compatibility
Unions of different types are incompatible. They cannot be assigned to each other (unless smart casting is employed) and they cannot be compared.