Patterns · Lesson 7.4

Struct pattern

Source
Take apart a variant case with named fields — .Circle { radius } => — binding, renaming, ignoring or testing each field.
You'll need: Variant, Variant match

Variant showed that a case can carry named fields, declared like a struct's, as in Between { low: float64; high: float64; }. Variant match then took cases apart by position, as in .Jump(across, _). Named fields deserve a pattern that uses their names: with two or three values of the same type, a name says far more than a position — is the second number the height or the base? This lesson shows that pattern.

Cases with named fields

variant Shape {
    Circle { radius: int32; },
    Rectangle { width: int32; height: int32; },
    Triangle { base: int32; height: int32; }
}

A value is built with the same braces, naming each field: Shape::Circle { radius: 5 }. The pattern that matches it looks just like that literal, with names where the values were.

Binding fields by name

The shorthand writes each field alone, and each one arrives as a variable of the same name:

func Area(shape: Shape) -> int32 {
    return match shape {
        .Circle { radius } => 3 * radius * radius,
        .Rectangle { width, height } => width * height,
        .Triangle { base, height } => base * height / 2
    };
}

A field written alone, radius, is short for radius: radius: "read the radius field into a variable called radius".

Renaming and leaving out

Fields are found by name, so their order in the pattern does not matter, and you are free to skip the ones you do not need:

func Width(shape: Shape) -> int32 {
    return match shape {
        .Circle { radius: r } => 2 * r,
        .Rectangle { width } => width,
        .Triangle { height: _, base } => base
    };
}

radius: r reads the radius into r. The rectangle leaves its height out, and a field left out matches anything, as if it were written height: _. The triangle writes height: _ anyway — that says "I know about this field and I do not need it" — and lists its fields in the opposite order from the declaration.

Testing a field

A literal in a field's place picks out the cases holding that value:

func Kind(shape: Shape) -> char8[..] {
    return match shape {
        .Circle { radius: 0 } => "dot",
        .Circle {} => "circle",
        .Rectangle {} => "rectangle",
        .Triangle {} => "triangle"
    };
}

Arms are tried in order, so the circle with radius 0 comes first and the plain circle arm below it catches every other radius. The other arms test nothing, and {} leaves every field out.

Four spellings

Field in the patternMeans
radiusbind the field to a variable of the same name
radius: rbind the field to a name of your choosing
height: _ or left outignore the field
radius: 0match only when the field holds this value

The program

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

Src/Main.rux
// A variant case can carry named fields, declared like a struct's: `Rectangle { width: int32;
// height: int32; }`. The pattern for such a case looks like the literal that builds it, with
// names where the values were: `.Rectangle { width, height } =>`.
//
// Each field in the pattern binds a variable or tests a value. Four spellings cover what you
// usually need:
//
//     .Circle { radius }                bind the field to a variable of the same name
//     .Circle { radius: r }             bind the field to a name of your choosing
//     .Rectangle { width }              leave out a field you do not need
//     .Circle { radius: 0 }             match only when the field holds this value
//
// Fields are found by name, so their order in the pattern does not matter. A field left out
// matches anything, as if it were written `height: _`, and `.Rectangle {}` leaves out every field.
// Writing `height: _` anyway says "I know about this field and I do not need it".
import Io::PrintLine;

variant Shape {
    Circle { radius: int32; },
    Rectangle { width: int32; height: int32; },
    Triangle { base: int32; height: int32; }
}

// The shorthand: each field arrives under its own name.
func Area(shape: Shape) -> int32 {
    return match shape {
        .Circle { radius } => 3 * radius * radius,
        .Rectangle { width, height } => width * height,
        .Triangle { base, height } => base * height / 2
    };
}

// Renaming and ignoring. `radius: r` reads the radius into `r`; the rectangle leaves out the
// height this question does not need; and the triangle skips it with `height: _`, listing its
// fields in the opposite order.
func Width(shape: Shape) -> int32 {
    return match shape {
        .Circle { radius: r } => 2 * r,
        .Rectangle { width } => width,
        .Triangle { height: _, base } => base
    };
}

// Testing. A literal in a field picks out the cases holding that value. Arms are tried in order, so
// the circle with a `0` comes first and the plain circle arm below it catches every other radius.
// The other arms test no field, so `{}` leaves every field out.
func Kind(shape: Shape) -> char8[..] {
    return match shape {
        .Circle { radius: 0 } => "dot",
        .Circle {} => "circle",
        .Rectangle {} => "rectangle",
        .Triangle {} => "triangle"
    };
}

func Main() -> int {
    let shapes: Shape[4] = [
        Shape::Circle { radius: 5 },
        Shape::Rectangle { width: 4, height: 6 },
        Shape::Triangle { base: 10, height: 3 },
        Shape::Circle { radius: 0 }
    ];
    for index in 0..4 {
        PrintLine("{:9} width {:2}, area {}", Kind(shapes[index]), Width(shapes[index]),
            Area(shapes[index]));
    }
    return 0;
}

Run it

cd Examples/Patterns/StructPattern
rux run
circle    width 10, area 75
rectangle width  4, area 24
triangle  width 10, area 15
dot       width  0, area 0

Common mistakes

A misspelt field name.
Fields are found by name, so a name the case does not have is an error: .Circle { radiuss } => stops with error: unknown field 'radiuss' in variant pattern.
Leaving out the braces.
.Circle => names a case with no data. For a case with fields it fails with error: pattern for 'Shape::Circle' expects 1 field, but found 0. Write .Circle {} to match any circle without reading its fields.
Testing a field without a catch-all arm.
.Circle { radius: 0 } covers only the circles of radius 0. If no plain .Circle {} (or .Circle { radius }) arm follows, the match fails with error: match on 'Shape' is not exhaustive; missing Shape::Circle.

Try it yourself

  1. Add a Square { side: int32; } case to Shape. Run rux check first and read which matches the compiler sends you to.
  2. Add an arm to Kind that names a rectangle whose width and height are equal a "square". You will need a guard.
  3. Write a function Perimeter that renames every field it reads, as in .Rectangle { width: w, height: h }.

Learn more