Control flow · Lesson 3.13

Match expression

Source
Use match as an expression whose arms produce the value.
You'll need: Match, Ternary

The conditional ? : chooses between two values; Ternary showed why that is better than declaring a var and assigning it in an if. match can do the same with any number of cases. Each arm produces a value instead of running a statement, and the whole match becomes the value of the arm that fit.

The long way: match as a statement

As a statement, choosing a value needs a var, declared first with a placeholder and assigned in every arm:

let day: int32 = 6;
var kindTheLongWay = "";
match day {
    6 => {
        kindTheLongWay = "weekend";
    },
    7 => {
        kindTheLongWay = "weekend";
    },
    else => {
        kindTheLongWay = "weekday";
    }
}

It works, but the name is repeated in every arm, nothing stops an arm from forgetting to assign it, and the result is a var that anything later could change.

The short way: match as a value

As an expression, each arm is just the value it produces:

let kind = match day {
    6 => "weekend",
    7 => "weekend",
    else => "weekday"
};

The binding is a let that gets its value where it is declared. Note the semicolon after the closing brace: the match is part of the let statement, and the statement needs its ;.

A value goes anywhere

Like any value, a match expression can be passed straight to a call. Most months have 31 days, so else covers the common case and the arms list the exceptions:

PrintLine("month {} has {} days", month, match month {
    2 => 28,
    4 => 30,
    6 => 30,
    9 => 30,
    11 => 30,
    else => 31
});

Or it can stand inside arithmetic — here, a price per cup size, multiplied by the number of cups:

let total = cups * match size {
    1 => 3,
    2 => 4,
    else => 5
};

One type, every value covered

A match expression has to produce exactly one value, of one type, whatever the input. That gives the compiler two things to check that a match statement does not need:

flowchart LR
    m(["let x = match value { … }"]) --> t{"Do all arms produce<br/>the same type?"}
    t -- "no" --> e1["error: match arm<br/>type mismatch"]
    t -- "yes" --> c{"Do the arms cover<br/>every possible value?"}
    c -- "no" --> e2["error: match on 'int32'<br/>is not exhaustive"]
    c -- "yes" --> ok["x gets the value<br/>of the arm that fits"]

A match over numbers cannot name every value, so it needs an else arm, and the compiler checks that it is there. The statement-form caveat from Match — an integer that fits no arm silently does nothing — cannot happen here, because "nothing" is not a value.

A bool match that names both true and false already covers every input, so it needs no else:

let sign = match open {
    true => "come in",
    false => "closed"
};
FormArms produceMust cover every value?Result
match statementstatementsno — unmatched does nothingnothing
match expressionvalues of one typeyes — else for numbersone value

The program

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

Src/Main.rux
// A `match` is also an expression: each arm can produce a value instead of running a statement,
// and the whole match becomes the value of the arm that fit. So it can initialize a `let`, be
// passed to a call, or appear in arithmetic, just like the `? :` conditional, but with any number
// of cases.
//
// Being one value, it has one type, so every arm must produce the same type:
//
//     let label = match day { 6 => "weekend", else => 0 };
//         error: match arm type mismatch: expected 'char8[..]', found 'int'
//
// And a value has to come out whatever the input, so the arms must cover every value. A match
// over numbers cannot name them all, so it needs an `else` arm, and the compiler checks:
//
//     let kind = match day { 6 => "weekend", 7 => "weekend" };
//         error: match on 'int32' is not exhaustive; its arms do not cover every value
//         help: add an 'else' arm
//
// A `bool` match that names both `true` and `false` already covers every input.
import Io::PrintLine;

func Main() -> int {
    // As a statement, choosing a value needs a `var`, declared first and assigned in every arm.
    let day: int32 = 6;
    var kindTheLongWay = "";
    match day {
        6 => {
            kindTheLongWay = "weekend";
        },
        7 => {
            kindTheLongWay = "weekend";
        },
        else => {
            kindTheLongWay = "weekday";
        }
    }

    // As an expression, the binding is a `let` that gets its value where it is declared. The
    // match ends with a semicolon here, because it is part of the `let` statement.
    let kind = match day {
        6 => "weekend",
        7 => "weekend",
        else => "weekday"
    };
    PrintLine("day {} is a {} ({} the long way)", day, kind, kindTheLongWay);

    // Passed straight to a call. Most months have 31 days, so `else` covers the common case.
    let month: int32 = 4;
    PrintLine("month {} has {} days", month, match month {
        2 => 28,
        4 => 30,
        6 => 30,
        9 => 30,
        11 => 30,
        else => 31
    });

    // Used inside arithmetic: a price per size, times a quantity.
    let size: int32 = 2;
    let cups: int32 = 3;
    let total = cups * match size {
        1 => 3,
        2 => 4,
        else => 5
    };
    PrintLine("{} cups of size {} cost {}", cups, size, total);

    // A `bool` match that names both values covers every input, so it needs no `else`.
    let open = true;
    let sign = match open {
        true => "come in",
        false => "closed"
    };
    PrintLine("the sign says {}", sign);
    return 0;
}

Run it

cd Examples/ControlFlow/MatchExpression
rux run
day 6 is a weekend (weekend the long way)
month 4 has 30 days
3 cups of size 2 cost 12
the sign says come in

Common mistakes

Arms of different types.
let label = match day { 6 => "weekend", else => 0 }; fails with error: match arm type mismatch: expected 'char8[..]', found 'int'. Every arm must produce the same type.
Missing the else arm.
let kind = match day { 6 => "weekend", 7 => "weekend" }; fails with error: match on 'int32' is not exhaustive; its arms do not cover every value, and the help says to add an else arm.
Forgetting the semicolon.
A match that initialises a let ends with };. Leave out the ; and the compiler stops at the next statement: error: expected ';' after the binding declaration before 'PrintLine'.

Try it yourself

  1. Write let name = match day { … }; that gives the name of each day from 1 to 7, with "unknown" for everything else.
  2. Make the month match return 29 for February in a leap year, using a bool leap and the conditional ? : inside the arm.
  3. Turn the cups example into a statement match that assigns a var, and compare the two versions.
  4. Remove the else arm from the month match and read the error.

Learn more

  • match in the Rux Reference
  • Exhaustive — how the compiler decides whether a match covers every case
  • Variant match — matching the cases of your own types