59 Variant types
The following is an example of a variant type:
variant Shape
{
Circle circle;
Triangle triangle;
Rectangle rectangle;
}
This represents a sum type. Although circle, triangle, and
rectangle can be regarded as alternatives, they are called attributes just as
for value and entity types, and they likewise are unary predicates having extensions.
The attributes do not exist conditionally: all three attributes exist for every Shape and always
have extensions. The variant constraint makes exactly one of those extensions non-empty.
Indeed, we could annotate the attributes with their natural language predicates using metadata:
variant Shape
{
@predicate("The shape is a circle value equal to [circle]")
Circle circle;
@predicate("The shape is a triangle value equal to [triangle]")
Triangle triangle;
@predicate("The shape is a rectangle value equal to [rectangle]")
Rectangle rectangle;
}
In this case the attribute types are nominal types. The attribute types may also be structural types, and a variant may mix these forms. For example:
variant Shape
{
{ Real radius; } sphere;
{ Real side; } cube;
{ Real radius; Real height; } cone;
→ Real volume = sphere.(4.0/3.0*pi*radius^3)
| cube.(side^3)
| cone.(pi*radius^2*height/3);
}
A variant type is closed. In any loaded schema version, the set of current base attributes is fixed by the declaration. To model an open variant, declare an abstract value or entity type and allow concrete types to subtype it.
Variant types do not have a value/entity distinction. A variant cannot declare a key, but it may
inherit one from an abstract entity supertype. A variant may declare abstract value types and
abstract entity types as supertypes using isa. The need to implement the inherited attributes
and key of an abstract entity is likely to limit the practical usefulness of abstract entity
supertypes for variants.
An inherited abstract attribute does not independently become another variant alternative. A directly declared base attribute may implement that inherited requirement; alternatively, an inherited requirement may be implemented by a derived attribute.
Variant types may declare derived attributes. The alternative operator | is often useful for
combining the results produced by different base attributes into one derived result, as in the
definition of volume above.
To support 117 Schema evolution in databases and serialisation formats, previously existing base attributes can be dropped, allowing for deserialising older versions. When we speak of the base attributes of a variant type we do not include the attributes that have been dropped.
Carrier set
Before applying declared and inherited constraints, the carrier induced by the variant representation is the labelled disjoint union of the carriers of its base attributes.
variant Shape
{
Circle circle;
Rectangle rectangle;
}
has carrier:
carrier(Shape) =
({circle} × carrier(Circle))
∪ ({rectangle} × carrier(Rectangle))
More generally, if:
W = variant { T1 A1; ...; Tn An; }
then:
carrier(W) =
⋃i=1n ({Ai} × carrier(Ti))
The attribute labels keep the summands distinct even when two attributes have the same underlying carrier.
Variant constraint
A variant type induces a mutual exclusivity constraint across its base attributes. For any instance, exactly one of those base attributes has a non-empty extension and every other base attribute has an empty extension. Derived attributes do not participate in this constraint.
This applies equally to attributes with the empty structural type {}.
Additional constraints may be specified either with where on an attribute
or in a constraints block.
The effective constraint on a variant type is the conjunction of its implicit mutual-exclusivity
constraint, all inherited constraints, and all directly declared constraints. These constraints
may mention multiple base attributes, including attributes that implement inherited abstract
attribute requirements.
Constraints inherited from an abstract supertype
Consider the following example:
abstract value Solid
{
{ Real radius; } sphere?;
{ Real side; } cube?;
constraints
{
// ∀S:{ Real radius; } ∀R:Real
// (this.sphere(S) ∧ S.radius(R)) → R > 0
sphere.(radius > 0);
// ∀C:{ Real side; } ∀L:Real
// (this.cube(C) ∧ C.side(L)) → L > 0
cube.(side > 0);
}
}
variant BasicSolid isa Solid
{
{ Float64 radius; } sphere where sphere.radius > 1;
{ Float64 side; } cube;
}
This deliberately contrived example illustrates several aspects of subtyping and constraints:
-
Sum types and product types are uniformly formalised through sets of attributes. This
provides a powerful unification: queries can navigate both forms uniformly, without
conditional field access. In this example, the sum type
BasicSolideven subtypes the product typeSolid. -
The abstract value type
Solidrequires attributes namedsphereandcube. For each required attribute, a concrete subtype must:- implement the attribute;
- use the same or a narrower cardinality constraint; and
- use the same attribute type or a subtype of it.
-
The abstract requirements use
?, so each predicate may have an empty or singleton extension. InBasicSolid, each base attribute has declared cardinality1when selected, but effective cardinality?because it is empty when another alternative is selected. It can therefore implement the corresponding abstract requirement without widening its cardinality. The implicit variant constraint additionally requires exactly one base attribute to be selected. -
BasicSolidimplements the payload types covariantly:{ Float64 radius; }and{ Float64 side; }are subtypes of the corresponding structural types containingReal. -
BasicSolidinherits both constraints declared bySolid. For each value, the constraint on the selected alternative must be satisfied; the constraint on the absent alternative is vacuously satisfied because its antecedent is false. -
The
whereclause onBasicSolid.sphereis an additional, directly declared constraint. It strengthens the inherited requirement fromradius > 0toradius > 1.
Payload-free attributes
A variant attribute which carries no application data may be written as a bare attribute name. For example:
variant PaymentResult
{
completed;
cancelled;
String declined;
{ String provider; String reference; } pending;
}
The declarations completed; and cancelled; are syntactic sugar for attributes whose payload
type is the empty
structural type {}. The declaration above is therefore equivalent to:
variant PaymentResult
{
{} completed;
{} cancelled;
String declined;
{ String provider; String reference; } pending;
}
The bare name is the attribute name and must be an identifier rather than a reserved
keyword. Although the source form omits a type, the attribute is not untyped: its effective
payload type is {}. When completed is selected its unary relation contains the single empty
structural value; otherwise that relation is empty. It can therefore be navigated and tested like
any other variant attribute.
In carrier-set terms, the payload-free summand contributes
{completed} × carrier({}) to carrier(PaymentResult). Its attribute label distinguishes it from
the other summands even though its payload carries no fields.
Cardinality constraints
Cardinality constraints apply within each variant base attribute in the usual way. The inactive current base attributes have empty extension, and the selected base attribute is the one with non-empty extension. Therefore a variant base attribute's declared cardinality must have lower bound at least one. For example:
variant Shape
{
Circle circle;
Point point+;
}
allows a shape to be either a single circle or a non-empty point set.
This implicitly means:
circlehas declared cardinality one when selectedpointhas declared cardinality one or more when selected- the variant constraint ensures that exactly one effective current base attribute has a non-empty extension
A variant base attribute cannot be declared with cardinality ? or *, because those constraints
allow an empty selected case. An empty extent already means the attribute is inactive.
To support an empty point set as a legitimate selected case, use a dedicated point set type and make the variant attribute a single value of that type:
variant Shape
{
Circle circle;
PointSet pointSet;
}
Generalisation to Algebraic Data Types
Algebraic Data Types are supported. For example:
@domain("A finite binary tree with integers at a subset of the leaf nodes")
variant BinaryTree
{
@predicate("The binary tree is an empty node")
{} empty;
@predicate("The binary tree is a leaf node holding the integer value [leaf]")
Integer leaf;
@predicate("The binary tree is a binary node equal to [node]")
{ BinaryTree left; BinaryTree right; } node;
}
Note:
- Recursive types are permitted
- Attributes may use the empty structural type
{}when they have no payload. These are ordinary unary attributes of type{}, similarly to enum attributes. - The attribute
nodehas a unary predicate and holds a unary relation. - The type of attribute
nodeis the structural type{ BinaryTree left; BinaryTree right; }.