Interfaces · Lesson 12.7

Structural equality

Source
Compare structs, tuples, arrays and variants with ==, field by field, without writing any code.
You'll need: Struct, Tuple, Variant, Comparison

== and != work on more than numbers. Two structs of the same type can be compared without writing anything at all: they are equal when every field is equal. The same goes for tuples, arrays and variants, and for any mix of them. This is called structural equality — equal shape, equal parts, equal values — and it is the right answer for plain data such as points and dates.

Field by field

let a = Point { x: 1, y: 2 };
let b = Point { x: 1, y: 2 };
let flipped = Point { x: 2, y: 1 };

a and b are two separate values, but field by field they are the same, so a == b is true. flipped has the same numbers in different fields: x is compared with x and y with y, so a == flipped is false and a != flipped is true.

One level down, and the next

A field that is itself a struct is compared by the same rule, one level down:

let first = Segment { start: a, end: flipped };
let second = Segment { start: b, end: flipped };
PrintLine("segments:      {}", first == second);
flowchart LR
    seg["first == second<br/>(Segment)"] --> s["start == start<br/>(Point)"]
    seg --> e["end == end<br/>(Point)"]
    s --> sx["x: 1 == 1"]
    s --> sy["y: 2 == 2"]
    e --> ex["x: 2 == 2"]
    e --> ey["y: 1 == 1"]

The comparison only ever bottoms out in values that have their own ==, such as integers. All four are equal, so the segments are too.

Tuples, arrays and variants

PrintLine("tuples:        {}", (a, true) == (b, true));
let left: int32[3] = [1, 2, 3];
let right: int32[3] = [1, 2, 4];
PrintLine("arrays:        {}", left == right);
let warm = Reading::Celsius(21);
let alsoWarm = Reading::Celsius(21);
let unknown = Reading::Missing;
Kind of valueEqual whenExample in the program
Structevery field is equal, compared in ordera == b is true
Tupleevery element is equal(a, true) == (b, true) is true
Arrayevery element is equal[1, 2, 3] vs [1, 2, 4] is false
Variantthe case matches, and so does its payloadwarm == alsoWarm is true, warm == unknown is false

Equality, not order

Structural comparison stops at == and !=. There is no field-by-field <: is a point with a bigger x but a smaller y "less"? The compiler will not guess. a < b fails with error: operator '<' is not defined for 'Point', and its note says why: "a struct is compared through the operators it declares, never by its representation".

Structural equality also has its limits. As Equatable showed, 1/2 and 2/4 have different fields but mean the same number. When a type means something else by "equal", it declares its own ==, which the next lesson, Operator overload, shows.

The program

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

Src/Main.rux
// `==` and `!=` work on more than numbers. Two structs of the same type can be compared without
// writing anything: they are equal when every field is equal, compared in order. The same goes
// for tuples, arrays and variants, and for any mix of them, because comparing a field that is
// itself a struct simply repeats the rule one level down.
//
// This is called structural equality: equal shape, equal parts, equal values. It is the right
// answer for plain data such as points and dates. When a type means something else by "equal",
// as 1/2 and 2/4 did in the Equatable lesson, it defines its own `==`, which the next lesson
// shows.
import Io::PrintLine;

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

struct Segment {
    start: Point;
    end: Point;
}

variant Reading {
    Missing,
    Celsius(int32)
}

func Main() -> int {
    let a = Point { x: 1, y: 2 };
    let b = Point { x: 1, y: 2 };
    let flipped = Point { x: 2, y: 1 };

    // Two separate values, field by field the same.
    PrintLine("a == b:        {}", a == b);
    PrintLine("a == flipped:  {}", a == flipped);
    PrintLine("a != flipped:  {}", a != flipped);

    // A struct inside a struct is compared by the same rule.
    let first = Segment { start: a, end: flipped };
    let second = Segment { start: b, end: flipped };
    PrintLine("segments:      {}", first == second);

    // Tuples and arrays compare element by element.
    PrintLine("tuples:        {}", (a, true) == (b, true));
    let left: int32[3] = [1, 2, 3];
    let right: int32[3] = [1, 2, 4];
    PrintLine("arrays:        {}", left == right);

    // Variants are equal when the case matches and so does the payload.
    let warm = Reading::Celsius(21);
    let alsoWarm = Reading::Celsius(21);
    let unknown = Reading::Missing;
    PrintLine("same reading:  {}", warm == alsoWarm);
    PrintLine("vs missing:    {}", warm == unknown);

    // Every part must have its own `==`, and a slice such as `char8[..]` has none. Add a
    // `name: char8[..]` field to Point and `a == b` is refused: "structural equality for 'Point'
    // is unavailable because element type 'char8[..]' has no '==' operator".
    return 0;
}

Run it

cd Examples/Interfaces/StructuralEquality
rux run
a == b:        true
a == flipped:  false
a != flipped:  true
segments:      true
tuples:        true
arrays:        false
same reading:  true
vs missing:    false

Common mistakes

A field with no == of its own.
Every part must be comparable, and a slice such as char8[..] is not. Add name: char8[..] to Point and a == b is refused: error: structural equality for 'Point' is unavailable because element type 'char8[..]' has no '==' operator. The compiler's help suggests the way out: compare the fields you can explicitly, or declare == for the type yourself.
Expecting < on a struct.
Only == and != come for free. a < b fails with error: operator '<' is not defined for 'Point'.
Expecting meaning, not shape.
A Fraction of 1/2 and one of 2/4 are not ==, because their fields differ. Field-by-field equality knows nothing about what a type stands for.

Try it yourself

  1. Make a third segment with start and end swapped. Is it == to first?
  2. Add a case Fahrenheit(int32) to Reading and compare Reading::Celsius(21) with Reading::Fahrenheit(21). Predict the answer first.
  3. Compare two arrays of Point, Point[2], that differ only in their last y.

Learn more