Types · Lesson 6.1

Struct

Source
Group named fields into a type of your own, and build values of it with a struct literal.
You'll need: Tuple, Function

A tuple groups values by position. A struct groups them by name, and gives the group a type of its own. A Point is not just any two numbers: its x cannot be mistaken for its y the way .0 can be mistaken for .1, and a function that asks for a Point cannot be handed a (int, int) that happens to mean something else.

This lesson is only about the data — declaring the fields, building a value, reading and writing the fields. Functions that belong to a struct come in Method, a few lessons on.

Declaring a struct

A declaration is struct, a name, and a list of fields in braces. Each field has a name, a colon, a type and a closing semicolon:

struct Point {
    x: int;
    y: int;
}

A field may have any type, including another struct:

struct Box {
    corner: Point;
    width: int;
    height: int;
}

The declarations sit at the top level of the file, beside the functions, and from then on Point and Box are types like int or bool.

A tuple (int, int)A struct Point
Fields are read by position: .0, .1Fields are read by name: .x, .y
Any two ints have the same typeA Point is its own type, with its name
Written on the spot, no declarationDeclared once, then used everywhere
Good for a quick pair from a functionGood for data that means something on its own

Building a value

A struct literal is the type's name and a value for every field, in braces:

let origin = Point { x: 0, y: 0 };
let corner = Point { y: 2, x: 5 };

Because every value is labelled, the fields may come in any order — corner lists y first. None may be left out, though: a struct value always has all of its fields, so Point { x: 1 } is refused.

A field holding a struct takes a struct value, and is read by chaining the dots:

let box = Box { corner: corner, width: 4, height: 3 };
PrintLine("box     at ({}, {}), {} by {}", box.corner.x, box.corner.y, box.width, box.height);

Changing a field

A field is assigned like a variable, and the same rule applies: the binding must be a var. Under let the whole struct is read-only, every field included.

var cursor = Point { x: 1, y: 1 };
cursor.x = 10;
cursor.y += 5;

A struct is a value

A struct can be passed to a function and returned from one, like any other value:

func Shifted(point: Point, dx: int, dy: int) -> Point {
    return Point { x: point.x + dx, y: point.y + dy };
}

Shifted is handed a copy of the point it is called with. It builds a new Point and returns that, so the caller's cursor comes out exactly as it went in:

flowchart LR
    cursor["cursor<br/>(10, 6)"] -- "copied in" --> point["point<br/>(10, 6)"]
    point -- "builds" --> result["Point (7, 10)"]
    result -- "returned" --> moved["moved<br/>(7, 10)"]
    cursor -. "unchanged" .-> after["cursor<br/>(10, 6)"]

For two numbers the copy costs nothing. For a large struct it does, and for a function that should change the caller's value a copy is no use at all — the next two lessons, Reference and Mutable reference, deal with both.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// A tuple groups values by position. A struct groups them by name, and gives the group a type of
// its own: a `Point` is not just any two numbers, and its `x` cannot be mistaken for its `y` the
// way `.0` can be mistaken for `.1`.
//
// This lesson is only about the data: declaring the fields, building a value with a struct
// literal, and reading and writing the fields. Functions that belong to a struct come a few
// lessons later.
import Io::PrintLine;

// A declaration lists the fields, each with its type and a closing semicolon.
struct Point {
    x: int;
    y: int;
}

// A field may have any type, including another struct.
struct Box {
    corner: Point;
    width: int;
    height: int;
}

// A struct is a type like any other, so a function can take one and give one back.
func Shifted(point: Point, dx: int, dy: int) -> Point {
    return Point { x: point.x + dx, y: point.y + dy };
}

func Main() -> int {
    // A struct literal is the type's name and a value for every field. The fields may come in
    // any order, but none may be left out: `Point { x: 1 }` is rejected for missing `y`.
    let origin = Point { x: 0, y: 0 };
    let corner = Point { y: 2, x: 5 };
    PrintLine("origin  ({}, {})", origin.x, origin.y);
    PrintLine("corner  ({}, {})", corner.x, corner.y);

    // A field holding a struct is read by chaining the dots.
    let box = Box { corner: corner, width: 4, height: 3 };
    PrintLine("box     at ({}, {}), {} by {}", box.corner.x, box.corner.y, box.width, box.height);

    // A field is assigned like a variable, and the same rule applies: the binding must be a
    // `var`. Under `let` the whole struct is read-only, every field included.
    var cursor = Point { x: 1, y: 1 };
    cursor.x = 10;
    cursor.y += 5;
    PrintLine("cursor  ({}, {})", cursor.x, cursor.y);

    // `Shifted` was handed a copy of `cursor` and built a new point, so `cursor` is unchanged.
    let moved = Shifted(cursor, -3, 4);
    PrintLine("moved   ({}, {})", moved.x, moved.y);
    PrintLine("cursor  ({}, {})", cursor.x, cursor.y);
    return 0;
}

Run it

cd Examples/Types/Struct
rux run
origin  (0, 0)
corner  (5, 2)
box     at (5, 2), 4 by 3
cursor  (10, 6)
moved   (7, 10)
cursor  (10, 6)

Common mistakes

Leaving a field out.
Point { x: 1 } fails with error: initializer for 'Point' is missing required field 'y'. A struct literal names every field; there are no defaults to fall back on. If building a value takes more than listing its fields, give the type a constructor.
Misspelling a field.
Point { x: 1, y: 2, z: 3 } fails with error: struct 'Point' has no field 'z', and a note lists the fields the struct does have.
Assigning a field of a let.
With let p = Point { x: 1, y: 2 };, the line p.x = 5; fails with error: cannot modify immutable variable 'p', and the compiler suggests declaring p with var. A field is part of its struct, and a let struct is read-only all the way through.
Printing a struct with {}.
PrintLine("{}", origin) fails with error: argument 2 to 'PrintLine' has type 'Point', but variadic parameter 'args' requires 'Display'. {} knows how to print numbers, text and booleans, not types you declared. Print the fields one by one — or, once you reach Display, teach the type to print itself.

Try it yourself

  1. Write func Mirrored(point: Point) -> Point that swaps x and y, and print Mirrored(corner).
  2. Write func Area(box: Box) -> int and print the area of box.
  3. Declare a Line struct with two Point fields, start and end, build one, and print all four numbers with chained dots.
  4. Change var cursor to let cursor and read the errors on the two assignments.

Learn more