Variants

Variants

A variant is a type that acts like a union of each type that declares itself a variant-member.
A variant is open-ended, somewhat like an abstract class in other languages. Or you could think of it as a union that's been scattered into separate declarations.

variant-member is not a subtype; the variant type and member type variant need explicit conversion.
It's also not inheritance. Making a type a variant-member doesn't alter the type; it alters the variant to have a new member.

variant-member parses like any other modifier and can be mixed with them. A type can have any number of variant-member declarations. So you could write: r record(xs nat mut[]) mut, foo variant-member, bar variant-member.

main void() info log 10.crow.describe info log duck.describe c crow = 11, info log c.to.describe bird variant # These could even go in a different module. crow record(height nat) bird variant-member duck record bird variant-member show string(a bird) match a as crow x "A {x.height}cm crow" as duck "A duck" # 'else' is required for variants else "Something else"

Each variant-member generates these functions:

  • A to function for converting to the variant.
  • A function with the type's name that converts back from the variant to an option of the type.
  • For records, a function with the type's name that constructs it as a variant directly. (So you can write 10.crow instead of (10,)::crow.to.)
main void() m m = 7, v v = m to back m? = v m if x ?= back info log "{x value}" () v variant m record(value nat) v variant-member

Methods

Since variants are open-ended, it's not possible to handle all members in a match.
If you do want to make sure every member implements something, the variant can declare methods. Then each variant-member must appear in a scope where the method is implemented for the member type.

Methods share the same syntax as spec signatures. Each method declared generates a function for calling it, where the variant is the first parameter.
Each method implementation should have the variant-memmber type as the first parameter.

main void() info log 10.crow.describe info log duck.describe bird variant show string() crow record(height nat) bird variant-member duck record bird variant-member show string(a crow) "A {a height}cm crow" show string(_ duck) "A duck"

Methods can take any number of parameters. The variant is always inserted at the front.

import crow/math/vec main void() shapes shape[] = ((5, 5) circle 5), (0 rect 10, 0, 10) for shape : shapes info log "{shape has-point (1, 1)}" shape variant has-point bool(point vec2) circle record(center vec2, radius float) shape variant-member rect record(left float, right float, bottom float, top float) shape variant-member has-point bool(a circle, point vec2) (point - a.center).length <= a.radius has-point bool(a rect, point vec2) a.left <= point.x && point.x <= a.right && a.bottom <= point.y && point.y <= a.top

Variants vs unions

When in doubt, you should use a union. Since all members of the union are known, you can match on it without needing an else branch.

Since you can convert from a variant to its member types, it's not the best option for an interface that is supposed to be only accessed through its methods. For that, instead use Lambdas.

The advantage of a variant is extensibility. For example, the code defining exception doesn't need to know every exception that might exist, so it's a variant to allow other code to define exceptions.
In the case of exceptions, we do want to be able to convert to the particular exception type, and not just rely on an interface common to all exceptions.