Types
A new type is created with the type
keyword. The simplest types are
tuple and record struct types. Tuple structs organize data strictly by
position, and record structs organize data strictly by label. When
constructing a record, the label names must be specified; however, they
may appear in any order.
// Tuple struct.
type PersonTuple(String, Int)
let person_tuple = PersonTuple("Cynthia", 29)
assert_eq(person_tuple.0, "Cynthia")
assert_eq(person_tuple.1, 29)
// Record struct.
type PersonRecord(name: String, age: Int)
let person_record = PersonRecord(age: 29, name: "Cynthia")
assert_eq(person_record.name, "Cynthia")
assert_eq(person_record.age, 29)
Algebraic types may have multiple constructors; however, a value of that
type is exactly one of them. Constructors are listed one per line. For
example, when a value of type Pet
is created, it must be exactly one
of Cat
, Dog
, or Fish
.
type Pet {
Cat
Dog
Fish
}
Algebraic types can also have data associated with them.
type Shape {
Circle(Float)
Rectangle(Float, Float)
Triangle(Float)
}
This type with tuple constructors can be instantiated and matched against.
import std/math
def area(shape: Shape) -> Float {
match shape {
Circle(r) => math::pi * r ** 2
Rectangle(w, h) => w * h
Triangle(s) => math::sqrt(3.0) / 4.0 * s ** 2
}
}
let r: Shape = Rectangle(3.0, 4.0)
assert_eq(area(r), 12.0)
Record constructors with labels may also be used.
type Shape {
Circle(radius: Float)
Rectangle(width: Float, height: Float)
Triangle(side: Float)
}
There is a shorthand for using labeled constructors when a local name is
the same as a label. When a single name is used to initialize a field,
such as width
, it is equivalent to writing width: width
. Since the
latter is redundant, it can be shortened to just width
. This applies to
record structs and algebraic record constructors.
let width = 3.0
let height = 4.0
let r = Rectangle(width, height)
Parameterized types
Type may be parameterized, and type parameters are listed in square
brackets [...]
.
type TuplePair[a, b](a, b)
type RecordPair[a, b](first: a, second: b)
type Groups[a, b, c] {
Single(a)
Pair(a, b)
Trio(a, b, c)
}
Recursive types
Types may store values that are of their own type. For example, the Node
s of
a Tree
store the left subtree, the node value, and the right subtree, while
each Leaf
simply stores a terminal value and ends recursion.
type Tree[a] {
Node(Tree[a], a, Tree[a])
Leaf(a)
}
// 4
// / \
// Create this tree 2 5
// / \
// 1 3
let tree: Tree[Int] = Node(Node(Leaf(1), 2, Leaf(3)), 4, Leaf(5))
Nonexhaustive types
An exhaustive type is an algebraic type for which all variants are known;
for example, Option[a]
can be either Some(a)
or None
; there will
never be a third variant.
If not all variants are known, then the type is nonexhaustive and should
be marked as such using the nonexhaustive
keyword. The compiler will
enforce that pattern matches on the type include a pattern to catch
variants that may be added in the future.
nonexhaustive type Color {
Black
White
Red
}
def hex_code(color: Color) -> String {
match color {
Black => "#000000"
White => "#ffffff"
Red => "#ff0000"
// Commenting out this line results in a compile time error
// because not all variants are handled.
_ => "unknown color"
}
}