Subset pattern
A typed pattern can name more than one member. The arm box: Square | Rectangle => matches when the active member is either of the two, and box is then a smaller sum: still one of several types, but only the ones the arm let through.
That is useful when some members are handled the same way. A circle has no corners, while a square and a rectangle both have four — so counting corners needs two arms, not three. And when the shared members still need work of their own, the smaller sum can be handed on whole to a function that takes exactly those members.
The shapes
Each shape is a struct of its own, and Shape is the sum of all three:
struct Circle {
radius: float64;
}
struct Square {
side: float64;
}
struct Rectangle {
width: float64;
height: float64;
}
type Shape = Circle | Square | Rectangle;
Several members in one arm
Write the members after the colon, joined by | just as in the type itself:
func Corners(shape: Shape) -> int32 {
return match shape {
_: Circle => 0,
_: Square | Rectangle => 4
};
}
A subset needs no binding either: _: Square | Rectangle only asks the question. The match is still exhaustive — between them, the two arms cover all three members.
The binding is a smaller sum
With a name instead of _, the arm binds what it matched. Here box has the type Square | Rectangle:
func Describe(shape: Shape) {
match shape {
c: Circle => PrintLine("circle, radius {}, {} corners", c.radius, Corners(shape)),
box: Square | Rectangle => PrintLine("box, area {}, {} corners", BoxArea(box),
Corners(shape))
}
}
box is not a square, and it is not a rectangle — it is whichever of the two shape held, and it remembers which. That is exactly what BoxArea takes, so box is passed on whole:
func BoxArea(box: Square | Rectangle) -> float64 {
return match box {
s: Square => s.side * s.side,
r: Rectangle => r.width * r.height
};
}
BoxArea only ever sees boxes, so it matches two members and no more. It never has to say what to do with a circle, because a circle cannot reach it.
flowchart LR
s["`shape: Shape`"] --> d{"`Describe`"}
d -- "`c: Circle`" --> c["radius, 0 corners"]
d -- "`box: Square | Rectangle`" --> b{"`BoxArea(box)`"}
b -- "`s: Square`" --> sq["`side * side`"]
b -- "`r: Rectangle`" --> re["`width * height`"]| Pattern | Matches when the active member is | The binding's type |
|---|---|---|
c: Circle | Circle | Circle |
box: Square | Rectangle | Square or Rectangle | Square | Rectangle |
_: Square | Rectangle | Square or Rectangle | nothing is bound |
The first matching arm wins
Arms are tried from the top, and the first one whose types include the active member runs. Two subsets may share a member — with round: Circle | Square => above box: Square | Rectangle =>, a square goes to the first arm and a rectangle to the second.
What the compiler refuses is an arm that can never run. Once box: Square | Rectangle has taken every square, a later s: Square => would be dead code, and it is an error rather than a silent leftover.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// A typed pattern can name more than one member. The arm `box: Square | Rectangle =>` matches
// when the active member is either of the two, and `box` is then a *smaller sum*: it is still
// one of several types, but only the ones the arm let through.
//
// That is useful when some members are handled the same way. A circle has no corners and a
// square and a rectangle both have four, so the corner count needs two arms, not three. And when
// the shared members need their own work, the smaller sum can be handed on whole to a function
// that takes exactly those members, and matched again there.
//
// An arm after a subset may not repeat a member the subset already took. Add `s: Square =>`
// after the `box` arm in `Describe` and the compiler refuses the arm that can never run:
// error: match arm is unreachable because earlier arms already match every value it matches
import Io::PrintLine;
struct Circle {
radius: float64;
}
struct Square {
side: float64;
}
struct Rectangle {
width: float64;
height: float64;
}
type Shape = Circle | Square | Rectangle;
// A subset needs no binding either: `_: Square | Rectangle` only asks the question.
func Corners(shape: Shape) -> int32 {
return match shape {
_: Circle => 0,
_: Square | Rectangle => 4
};
}
// This function only ever sees boxes, so it matches two members and no more.
func BoxArea(box: Square | Rectangle) -> float64 {
return match box {
s: Square => s.side * s.side,
r: Rectangle => r.width * r.height
};
}
func Describe(shape: Shape) {
match shape {
c: Circle => PrintLine("circle, radius {}, {} corners", c.radius, Corners(shape)),
box: Square | Rectangle => PrintLine("box, area {}, {} corners", BoxArea(box),
Corners(shape))
}
}
func Main() -> int {
let wheel: Shape = Circle { radius: 1.5 };
let tile: Shape = Square { side: 2.0 };
let door: Shape = Rectangle { width: 1.0, height: 2.5 };
Describe(wheel);
Describe(tile);
Describe(door);
return 0;
}
Run it
cd Examples/SumTypes/SubsetPattern
rux run
circle, radius 1.5, 0 corners
box, area 4.0, 4 corners
box, area 2.5, 4 corners
Common mistakes
Try it yourself
- Add
struct Triangle { base: float64; height: float64; }toShape. Which matches does the compiler now reject, and what does each one need? - Write
func IsRound(shape: Shape) -> boolwith exactly two arms. - Move
s: Square =>above theboxarm inDescribe, so squares print differently from rectangles. Which members can still reachboxnow?
Learn more
- The is operator — ask which member a sum holds without unpacking it
- Sum widening — passing a smaller sum where a larger one is expected
matchin the Rux Reference