Note: None of this capability is currently implemented.
From a type perspective, the structure of a struct or array value is quite fixed. This is restrictive when we need values whose structure can vary based on runtime logic. This is what enum (and union-based) types address. They allow a value's type structure to vary as signalled by its captured "tag".
Enums hold a single value whose type could be one of several variations. For example:
enum Number Integer i32 Float f32
This defines a new enum type called Number which holds a single numeric value. That value could be an integer or a floating-point number, but not both. Unlike with structs, Integer and Float are not separate fields stored in different locations. They are instead two possible overlays on a single memory location.
Under the covers, enum values also hold a hidden field called the "tag". In this example, the tag indicates whether the value currently holds an integer (tag field = 0) or a floating-point number (tag field = 1).
As the example shows, an enum type declaration resembles a struct declaration. It begins by giving a name to this new, custom-defined type. The variant types options are listed within, each specifying the option's:
- Name. As the option name represents a type, it is typically capitalized. Names should not begin with (or be) an underscore. A name may be the same as the name of the type specified for this variant.
- Type. This is the type of any value of this option. A type need not be specified when this option is not attached to a value. Alternative, the specified may be a set of fields enclosed in curly braces. This is understood to mean the type is a struct that includes the defined fields.
- Tag Value. Normally, a number is automatically assigned to each variant for the tag, starting with 0 and incrementing upwards on each option. This tag number may be explicitly indicated, if desired.
This declaration specifies three variant types:
enum OddEnum None // No value for this variant SomeInt i32 // Value is an integer PointStruct // Value is a struct holding two floats x f32 y f32
Note: enums can be used to associate names to constant numbers. Just leave out type information:
enum Colors Red = 0xFF0000 Green = 0x00FF00 Blue = 0x0000FF
An enum initial value is built using the enum constructor:
mut oddval = OddEnum::None oddval = OddEnum::SomeInt oddval = OddEnum::PointStruct[x: 4., y: 8.]
An enum value may be passed around, by copy, just like any other value. This is possible because all variant values of a specific enum type are the same size. The enum type's size is effectively the size of its largest variant type plus the size of its hidden tag field.
To safely gain access to the differently-typed value enclosed with an enum value, we must use pattern matching. As a brief peek into the versatile power of partial pattern matching, consider this example:
match oddval ~~ intval = SomeInt: intval ~~ point = PointStruct: i32[point.x * point.y] None: 0
The match statement allows us to determine which type of data this OddEnum value holds. If it is an integer, it will match on the first option and (as part of the match) copy that integer into a new variable called intval. For this variant, the logic indicates that the match expression should evaluate to this integer's value. And so it goes for matching against any of the other variants.
One may also match an enum value against a single variant using an if statement:
sum += intval if oddval ~~ intval=SomeInt
Note:If an enum value is being matched against a variant that has no value (e.g., none above), the equivalence operator == may be used instead of the partial match operator ~~, if desired.
An enum value may only be compared for equivalence (and not order). The equivalence check compares the tag fields first. If they are equal, the appropriately-typed interior values are compared using the == method.
Option and Result Types
Two particular enum patterns are so pervasive that special support is baked in to make them easier to use:
- Result. The value can be either a valid value or an exception value.
- Option. The value can be either a valid value or null ("not a value").
In our earlier discussion about exception handling, we introduced the idea that some functions can choose to throw an exception value on failure and a regular value on success. This is actually just a Result value being returned. Built-in language syntax just makes it clearer and simpler to work with such values:
- A function whose return signature is i32 throws Bool is another way to write a Result type: Result<i32, Bool>
- return 3 is equivalent to return ok.
- throw false is equivalent to return err[false].
The ? operator (try) and || operator (or else) offer more concise and readable ways to perform various common patterns for applying pattern matching blocks against a Result value.
The Option type is useful when we want to express the idea that we may or may not have a value of a certain type. null is the name used to represent the absence of a value. some[x] represents some specific value. Nullable values are needed so often that special syntax exist to make them easier to use.
To declare a nullable type, simply put a question mark before the type. For example:
mut maybeInt ?i32 = null // Initial value indicates the absence of a value if maybeInt == null // or: if !maybeInt maybeInt = 4 // Give it the value 4
If the type of a declared variable is omitted but an initial value is specified, the initial value needs to be wrapped in the some constructor:
mut maybeInt = some
Nullable References Although we have not covered references yet, it is worth nothing here that nullable references take no more space than regular references. The null reference value is just a non-addressable address (typically, 0).
The ? and || operators that are so helpful for Result values can also be applied to nullable values.
Give null a default value
Use the || operator to establish a real value in the event of null.
imm val = maybeInt || 0
val will be the value of maybeInt if it is not null. Otherwise it will be 0. The value to the right of the operator cannot be a nullable type, and its type must match that of a valid value for the nullable value on the left.
Handle null as an exception
Use the ? operator to treat a null value as an exception.
imm val = maybeInt?
val will be the value of maybeInt if it is not null. Otherwise it will panic.
If a catch null handler is provided, it will be invoked instead of a panic.
A variation of the method call operator allows safe method calls on a nullable value:
imm x = maybePoint?.dist
In effect, this propagates nullability from maybePoint to x. If maybePoint holds a valid value, the method is called and its return value is wrapped as nullable. If maybePoint is null, then so will x be.