Unions
The union type provides the foundation for sum types in SuperSQL.
Unions conform with the definition of the union type in the super-structured data model and follow the syntax of unions in the SUP format, i.e., a union type has the form
<type> | <type>, ...
where <type> is any type and the set of types are unique.
A union literal can be created by casting a literal that is a member of a union type to that union type, e.g.,
1::(int64|string)
When the type of the value cast to a union is not a member of that union, an attempt is made to coerce the value to one of available member types.
To precisely such control coercion, an explicit first cast may be used as in
1::int8::(int8|int64|string)
Union values can be created by reading external data (SUP files,
database data, JSON objects, etc),
by constructing instances with a type cast
as above, or with other SuperSQL functions or expressions that produce unions
like the fuse operator.
Union values are also created when array, set, and map expressions encounter mix-typed elements that automatically express as union values.
For example,
values typeof([1,"foo"])
results in <[int64|string]>.
Union Value Semantics
Internally, every union value includes a tag indicating which of its member types the value belongs to along with that actual value.
In many languages, such tags are explicit names called a discriminant
and the underlying value can be accessed with a dot operator, e.g.,
u.a where u is a union value and a is the discriminant. When
the instance of u does not correspond to the type indicated by a,
the result might be null.
In other languages, the discriminant is the type name, e.g.,
u.(int64).
However, SuperSQL is polymorphic so there is no requirement to explicitly discriminate the member type of a union value. When an expression operator or function references a union value in computation, then the underlying value in its member type is automatically expressed from the union value.
For example, this predicate is true
values 1::(int64|string)==1
because the union value is automatically expressed as 1::int64
by the comparison operator.
Likewise
values 1::(int64|string)+2::(int64|string)
results in 3::int64. Note that because of automatic expression,
the union type is not retained here.
Passing a union value to a function, however, does not involve evaluation and thus automatic expression does not occur here, e.g.,
values typeof(1::(int64|string))
is <int64|string> because the union value is not automatically
expressed as 1::int64 when it is passed to the
typeof function.
When desired, the under function may be
used to express the underlying value explicitly. For example,
values typeof(under(1::(int64|string)))
results in <int64>.
Union Dispatch
Languages with sum types often include a construct to dispatch the union to a case for each of its possible types.
Because SuperSQL is polymorphic, union dispatch is not generally needed. Instead, union values are simply operated upon and the “right thing happens”.
That said, union dispatch may be accomplished with the
switch operator or a
case expression.
For example, switch can be used to route union values to different
branches of a query:
values
{u:1::(int64|string|ip)},
{u:"foo"::(int64|string|ip)},
{u:192.168.1.1::(int64|string|ip)}
| switch typeof(under(u))
case <int64> ( ... )
case <string> ( ... )
case <ip> ( ... )
default ( values error({message: "unknown type", on:this}) )
| ...
Note
Note the presence of a default case above. In statically typed languages with sum types, the compiler can ensure that all possible cases for a union are covered and report an error otherwise. In this case, there would be no need for a default. A future version of SuperSQL will include more comprehensive compile-time type checking and will include a mechanism for explicit union dispatch with static type checking.
A case expression can also be used to dispatch union values inside of
an expression as in
values
{u:1::(int64|string|ip)},
{u:"foo"::(int64|string|ip)},
{u:192.168.1.1::(int64|string|ip)}
| values
case typeof(under(u))
when <int64> then u+1
when <string> then upper(u)
when <ip> then network_of(u)
else "unknown"
end
Examples
Cast primitive values to a union type
# spq
values this::(int64|string)
# input
1
"foo"
# expected output
1::(int64|string)
"foo"::(int64|string)
Explicitly express the underlying union value using
under
# spq
values under(this)
# input
1::(int64|string)
# expected output
1
Take the type of mixed-type array showing its union-typed construction
# spq
typeof(this)
# input
[1,"foo"]
# expected output
<[int64|string]>