Structural equality
== 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 value | Equal when | Example in the program |
|---|---|---|
| Struct | every field is equal, compared in order | a == b is true |
| Tuple | every element is equal | (a, true) == (b, true) is true |
| Array | every element is equal | [1, 2, 3] vs [1, 2, 4] is false |
| Variant | the case matches, and so does its payload | warm == 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.
// `==` 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
== 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.< on a struct.Only
== and != come for free. a < b fails with error: operator '<' is not defined for 'Point'.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
- Make a third segment with
startandendswapped. Is it==tofirst? - Add a case
Fahrenheit(int32)toReadingand compareReading::Celsius(21)withReading::Fahrenheit(21). Predict the answer first. - Compare two arrays of
Point,Point[2], that differ only in their lasty.
Learn more
- Comparison —
==and!=on numbers - Struct, Tuple and Variant — the values compared here
- Operator overload — giving a type its own
== - Comparison operators in the Rux Reference