Similar to / also known as: sum type, enum, enumeration, oneof.
It is possible to specify that one of several types should be used.
record Network: socket: Socket secure: Boolean record Disk: fh: FileReadHandle union DataSource = Network | Disk
In this case, the
DataSource is either a network or a disk instance.
This could also be achieved by doing:
union DataSource: record Network: socket: Socket secure: Boolean record Disk: fh: FileReadHandle
The only difference is that
Disk are inside the namespace
Unless explicitly implemented, unions automatically implement those trait that are implemented by every variant, by delegating to the current variant.
Tagged vs untagged
There are some differences between these 'one of' types in different languages. Many languages have nothing, or only have enums that cannot hold instance data.
Of languages that have more expressive sum union types, there are two variants:
- Tagged (sum types): each variant has a name and so can be distinguished, even if the types or values are the same. E.g. the type would not be
String | int | int, but rather
A(String) | B(int) | C(int).
- Untagged (union types): possible values are all values of member types, with duplicates removes. So
String | int | intis the same as
String | int, as the two integer variants are indistinguishable.
In Mango, it is tracked which variant is present. But the tag is not part of the union, the type is used instead.
This means that Mango unions behave a bit like untagged unions, in that
String | int | int is not a valid union type. But unlike the (untagged) unions in C, the variant is stored and can be found for any union instance.
You can create your own tags easily, and you are usually encouraged to do so:
union Tagged: record A(String) record B(int) record C(int)
The advantages of using the type as 'tag' are less verbosity and the ability for a type to be part of multiple unions.
Unions vs trait impl
This has some parallels to inheritance, or to implementing traits.
union U = A | B | C are assignable to variables of type
U, and nothing else is.
The same would be true if
U were a trait and
C the only records implementing it.
Indeed, languages like Kotlin implement sum types as a special form of inheritance (using
sealed). In Mango the parallel is less explicit.
The biggest difference with between union variants and trait implementations, is that union variants form a _closed_ group: all members are known and fixed.
This has the result that downcasting only makes sense for unions, not for traits (there are technical reasons as well). Downcasting a trait to a specific record would mean that that record is treated differently from any other implementation of the trait. This is an odd choice to make if you can't know which implementations there are (because anyone could have created one).
Another difference in some languages, is that a record can implement any number of traits, but a variant can only belong to one union. This is _not_ the case in Mango, a record can belong to multiple unions:
record A record B record C union P = A | B union Q = B | C
- While there are expressive ways to require combinations of types, there is no general way to require the _absense_ of a type (i.e. no way to make sure that a trait is not implemented for a record or union).
- With product types (records) and sum types (unions), Mango has algebraic types.