Learn · Chapter 6 of 12

Structs and sum types

Two ways to define your own types: a record with named fields (struct) and a tagged union with named variants (sum). Pattern matching becomes powerful here.

Structs: a record of named fields

type Point {
    x: Int,
    y: Int
}

Construct an instance with curly-brace syntax that names each field, like a JSON object:

fun main(stdio: Stdio)
    let p = Point { x: 3, y: 4 }
    stdio.println("(${p.x}, ${p.y})")

Field access uses .. Field order does not matter at construction, but every field must be provided. There are no default values; if you want a "zero point", define a function that returns one.

Structs are nominal: two structs with identical field shapes are different types. A Point2D with x: Int, y: Int is not interchangeable with a Vector2 of the same shape. This is deliberate; named types catch confusions at the type level.

Methods on a struct

An impl block attaches methods to a type:

type Point {
    x: Int,
    y: Int
}

impl Point
    fun distance_from_origin(self) -> Float
        let sq = to_float(self.x * self.x + self.y * self.y)
        return sq   // (a real sqrt would live in math; this is illustrative)

Inside an impl, self is the instance. Methods are called on values with .:

fun main(stdio: Stdio)
    let p = Point { x: 3, y: 4 }
    stdio.println("${p.distance_from_origin()}")

An impl can be inherent (impl Point, methods on Point) or of a trait (impl SomeTrait for Point, satisfying the trait's interface). Traits arrive in chapter 10.

Sum types: one-of-these tagged variants

A sum type lists a closed set of named variants. Each variant can optionally carry a payload:

type Shape =
    Circle(Float)
    Rectangle(Float, Float)
    Square(Float)

Construct a value by naming the variant and passing its payload:

let c = Circle(5.0)
let r = Rectangle(3.0, 4.0)

A variant can also be payloadless:

type Color =
    Red
    Green
    Blue

let c = Red

Pattern matching

The natural way to operate on a sum value is match. Each arm names a variant and binds its payload:

fun area(shape: Shape) -> Float
    match shape
        Circle(r) ->
            return 3.14159 * r * r
        Rectangle(w, h) ->
            return w * h
        Square(side) ->
            return side * side

The arms bind r, w, h, side to the corresponding payload of the matched variant. Inside that arm, those are normal local Float values.

match on a sum is checked for exhaustiveness. If you forget a variant, the compiler refuses:

fun area(shape: Shape) -> Float
    match shape
        Circle(r) -> return 3.14159 * r * r
        Rectangle(w, h) -> return w * h
error: non-exhaustive match: missing variant 'Square'
   2 |     match shape
                ^

This is one of the most useful guarantees in the language: when you add a new variant to a sum, every match on that sum that did not have a wildcard becomes a compile error until you handle the new case. Refactors that would silently miss a branch in Python become impossible.

Generics: sum types with type parameters

The built-in Option<T> and Result<T, E> are sum types parameterised by their payload type. Your own types can be too:

type Tree<T> =
    Leaf
    Node(T, Tree<T>, Tree<T>)

A Tree<Int> is a tree of integers, a Tree<String> a tree of strings. The type parameter T is bound at use time.

Try this. Define a type Triangle sum with variants Equilateral(Float) (side length), Right(Float, Float) (legs), and Scalene(Float, Float, Float). Write a function area(t: Triangle) -> Float that handles all three.

The two error modes

First, the exhaustiveness check we already saw: a match on a sum must cover every variant unless you write a _ wildcard arm. The compiler points at the missing one by name.

Second, missing fields when constructing a struct:

type Point {
    x: Int,
    y: Int
}

fun main(stdio: Stdio)
    let p = Point { x: 3 }
    stdio.println("${p.x}")
error: struct literal 'Point': missing field 'y'
   5 |     let p = Point { x: 3 }
                   ^

Every field of the struct must appear in the literal. No defaults; if you want one, write a constructor function that fills the missing fields.

Where you are now

You can model domains with your own types. The next chapter formalises the most common reason a function returns a sum type rather than a plain value: this operation might fail. Option, Result, and the ? operator.