Skip to content

Structs, variants and enums ​

struct: a product type ​

talor
struct Point { x: f64, y: f64 }
struct Wrapper<T> { value: T, count: i64 }

let p = Point { x: 1.0, y: 2.0 };
let q = Point { x, y };            // shorthand when a variable has the field's name

Struct fields are separated by ,; a trailing comma is allowed. A struct is a value type with a fixed layout. Every struct literal names its type; there are no anonymous object types. Two structs with the same fields are different types.

variant: a tagged union ​

A variant is a sum type: a value is exactly one of the cases listed, never two, and each case carries its own data.

talor
variant Shape {
    Circle(f64),                    // positional payload
    Rect { w: f64, h: f64 },        // named payload
    Empty,                          // no payload
}

variant Tree<T> { Leaf(T), Node(Box<Tree<T>>, Box<Tree<T>>) }

The layout is { i32 tag, payload }, where the payload is a block the size of the largest case, the way a C union shares its members. What C does not have is the tag: the compiler writes it at construction and checks it before every read. There is no syntax that reads a payload without that check - a case is read only through match.

A case is constructed by name through its type, with the dot: Shape.Circle(1.0). There is no s.Circle on a value, and no :: anywhere.

Option<T> and Result<T, E> are variants, which is why Some(p), None, Ok(p) and Err(e) work as patterns everywhere.

enum: a numbered set ​

An enum is the other half: no case carries data, and each has an integer value. That is the C construct the word names, and the two do not overlap: a variant needs at least one case that carries data, an enum refuses any, and each refusal names the other word.

talor
enum Errno { Interrupted = 4, NotFound = 2, Denied = 13 }
enum Color { Red, Green, Blue }         // 0, 1, 2

A case without a value takes one more than the previous; the first takes zero. Two cases may not share a value. The value is the tag, so an enum is one i32 and readable both ways:

talor
let n = Errno.Denied as i64;            // 13
let name = match Errno.of(2) {
    Some(Errno.NotFound) => "not found",
    Some(_) => "another case",
    None => "unknown",                  // no case has that value
};

E.of(n) answers Option<E>, never a cast: an arbitrary number is not a case, and the check is what keeps the type honest.

Recursion ​

A recursive type needs no word:

talor
variant Tree { Leaf(i64), Node(Tree, Tree) }

The compiler puts the edge that closes the cycle behind an indirection automatically, so the programmer writes the value and never sees the box. A mutual cycle gets the same treatment, and so does a ring of three types or more: the member that closes the cycle carries the indirection.

talor
variant Expr { Num(i64), Neg(Stmt) }
variant Stmt { Eval(Expr), Skip }

fn depth(e: Expr): i64 {
    match e {
        Expr.Num(_) => 1,
        Expr.Neg(s) => match s { Stmt.Eval(x) => 1 + depth(x), Stmt.Skip => 1 },
    }
}

fn main(): i32 {
    let e = Expr.Neg(Stmt.Eval(Expr.Num(3)));
    println(`${depth(e)}`);
    0
}

A programmer who wants the indirection somewhere else writes Box<T> there.

Equality ​

== and != compare a value part by part, in declaration order, and stop at the first part that differs. The walk follows a part onto the heap: a string compares its characters, an Array its elements, a Box what it points at - which is how a recursive type gets an equality at all.

A type whose value is not its representation says so, with a pair:

talor
struct Pair { a: i64, b: i64 }        // a set of two numbers, so order is not value

impl Pair {
    fn eq(self, other: Pair): bool {
        (self.a == other.a && self.b == other.b) || (self.a == other.b && self.b == other.a)
    }
    fn hash(self): u64 { panic("the body") }
}

The two come in a pair, and a type that declares one without the other is refused: a container that compares by one rule and hashes by another stores an entry under the first and looks for it under the second. Only == and != - there is no order on an aggregate.

A closure has no equality, and neither does an extern type handle or any value that holds one of them; comparing one is refused.

What every type gets ​

A struct, a variant or an enum the program declares has .clone(), ==, hash(x) and .debug() with nothing written: .debug() answers the value as text, P { x: 1, y: 2 } or Shape.Rect(1, 2). A type that holds something that cannot be cloned or compared lacks the operation that needs it.

Talor v0.1.0 - Released under the MIT OR Apache-2.0 license.