Structs, enums and traits

Three ways to make a type: a struct holds several values at once, an enum holds one of several cases, and a trait names what a set of types can do. With generics, they are the whole type system a program needs.

topo/code/shapes.nx
/// A struct: named fields, a value type.
struct Point derive(Eq) { x: f64, y: f64 }

impl Point {
    fn origin() -> Point { return Point{ .x = 0.0, .y = 0.0 } }

    fn distance(self: *Self, other: *Point) -> f64 {
        let dx = self.x - other.x
        let dy = self.y - other.y
        return math.sqrt(dx * dx + dy * dy)
    }

    fn shift(self: *mut Self, dx: f64, dy: f64) {
        self.x += dx
        self.y += dy
    }
}

/// An enum: one of several cases, some with data.
enum Shape {
    Circle(f64),
    Rect { w: f64, h: f64 },
    Triangle(f64, f64, f64),
    Empty,
}

fn area(s: *Shape) -> f64 {
    match s.*{
        .Circle(r) => math.PI * r * r,
        .Rect(w, h) => w * h,
        .Triangle(a, b, c) => {
            let p = (a + b + c) / 2.0
            math.sqrt(p *(p - a) *(p - b) *(p - c))
        },
        .Empty => 0.0,
    }
}

/// A trait: a set of methods a type promises.
trait Named {
    fn name(self: *Self) -> String
}

impl Named for Point {
    fn name(self: *Self) -> String { return format("point ({}, {})", .{self.x, self.y}) }
}

impl Named for Shape {
    fn name(self: *Self) -> String {
        return match self.*{
            .Circle(_) => String.from("circle"),
            .Rect(_, _) => String.from("rectangle"),
            .Triangle(_, _, _) => String.from("triangle"),
            .Empty => String.from("nothing"),
        }
    }
}

/// A generic function: `T` is a type parameter, checked once per use.
fn largest(comptime T: type where T: Ord, items: []T) -> ?T {
    if items.len == 0 { return null }
    var best = items[0]
    for x in items { if x > best { best = x } }
    return best
}

/// A generic struct.
struct Pair(T) { first: T, second: T }

impl(T) Pair(T) {
    fn swapped(self: *Self) -> Pair(T) { return Pair(T){ .first = self.second, .second = self.first } }
}

fn describe(thing: dyn Named) {
    println("this is a {}", .{thing.name()})
}

fn main() {
    var p = Point{ .x = 3.0, .y = 4.0 }
    let o = Point.origin()
    println("{} from the origin", .{p.distance(&o)})
    p.shift(1.0, 1.0)
    println("moved to ({}, {}); equal to origin: {}", .{p.x, p.y, p == o})

    let shapes = [Shape.Circle(1.0), Shape.Rect{ .w = 2.0, .h = 3.0 }, Shape.Triangle(3.0, 4.0, 5.0), Shape.Empty]
    for s in shapes { println("{}: {:.2}", .{s.name(), area(&s)}) }

    let numbers = [3, 9, 4]
    println("{}", .{largest(i32, numbers[..]) orelse 0})
    let none = List(f64).new()
    println("{}", .{largest(f64, none[..]) orelse -1.0})
    let words = ["pear", "apple", "fig"]
    println("{}", .{largest([]u8, words[..]) orelse ""})

    let pair = Pair(i32){ .first = 1, .second = 2 }
    let sw = pair.swapped()
    println("{} {}", .{sw.first, sw.second})

    // trait objects: one function, any type that implements the trait
    describe(&p)
    describe(&shapes[0])
}
topo/code/shapes.expected
5.0 from the origin
moved to (4.0, 5.0); equal to origin: false
circle: 3.14
rectangle: 6.00
triangle: 6.00
nothing: 0.00
9
-1.0
pear
2 1
this is a point (4.0, 5.0)
this is a circle

Structs and methods#

struct Point derive(Eq) { x: f64, y: f64 }

A struct is a value: assigning one copies its fields, and two Points are two Points. Fields are separated by commas or newlines and may have defaults (verbose: bool = false). derive(Eq) makes == work field by field; derive(Ord) adds < and friends, and derive(Hash) lets the type be a map key.

Methods live in an impl block. The receiver is spelled out:

p.distance(&o) passes a pointer to o because the parameter is *Point; p.shift(1.0, 1.0) takes &mut p for you. Field access through a pointer needs no arrow: self.x and other.x both just work.

Enums#

enum Shape {
    Circle(f64),
    Rect { w: f64, h: f64 },
    Triangle(f64, f64, f64),
    Empty,
}

An enum value is exactly one of its cases, and a case may carry data: a tuple of values (Circle(f64)), named fields (Rect { w, h }), or nothing (Empty). Construct with Shape.Circle(1.0) or Shape.Rect{ .w = 2.0, .h = 3.0 }. The only way to look inside is match:

    match s.* {
        .Circle(r) => math.PI * r * r,
        .Rect(w, h) => w * h,
        ...
        .Empty => 0.0,
    }

.Circle(r) binds the payload. The match must cover every case; leave one out and the compiler names it. That is the point of enums over "an integer tag and some fields": adding a case later makes every match that forgot it a compile error, not a silent fall-through.

s.* reads through the pointer s: *Shape. Matching through a pointer binds the payloads by reference, which matters when they own something (a List inside a case is not copied; chapter 8).

An enum whose cases are all empty (a plain "one of these") casts to an integer with as and back with a match.

Traits and dyn#

trait Named {
    fn name(self: *Self) -> String
}

impl Named for Point { ... }
impl Named for Shape { ... }

A trait is a list of method signatures. impl Trait for Type provides them, and from then on p.name() works on a Point. Traits are how generic code states what it needs (where T: Ord below), and how one function can accept different types at run time:

fn describe(thing: dyn Named) {
    println("this is a {}", .{thing.name()})
}

dyn Named is a trait object: a pointer to any value whose type implements Named, plus a table of that type's methods. describe(&p) and describe(&shapes[0]) pass a Point and a Shape through the same parameter, and the call dispatches at run time. A List(dyn Named) holds a mixed collection. Trait objects carry effects too: dyn Named !allocates accepts only implementations that do not allocate (chapter 15).

Generics#

fn largest(comptime T: type where T: Ord, items: []T) -> ?T {

A generic function takes a type as a comptime parameter. where T: Ord says the function will compare values, so only ordered types are accepted; the call names the type, largest(i32, numbers[..]), and the compiler generates one version of the function per distinct type it is called with (monomorphization: no boxing, no runtime type information). Generic structs look the same: Pair(T), constructed as Pair(i32){ ... }, with methods in impl(T) Pair(T).

The standard library's std.lists is written this way (lists.map(i32, i32, xs[..], double)), which is why its functions take the element type first.

Other declarations#

Two you will meet in later chapters and in other people's code:

type Meters = distinct f64       // a new type with f64's representation and no implicit conversion
record Dose { mg: f64 where value > 0.0 }   // a struct whose invariants are checked when it is built
ref class Node { value: i32, next: ?Node }  // a reference-counted object (chapter 8)

Next: ownership, the chapter that makes the language what it is.