Sum types · Lesson 10.4

The is operator

Source
Ask which member a sum holds — token is Number, token is (Plus | Minus) — and get a bool back, without unpacking anything.
You'll need: Subset pattern, For, Array

Sometimes a program only needs to know which member a sum holds, not what is inside it. Is this token a number? Is it an operator? value is T answers with a plain bool: true when the active member is T.

A typed pattern can answer the same question, but it takes a whole match with an arm for every member. is is the one-line version for when a yes or no is all you want.

The tokens

A tokenizer turns the text price + 2 - discount + 1 into pieces, and each piece is one of four types:

struct Number {
    value: int32;
}

struct Name {
    text: char8[..];
}

struct Plus {}

struct Minus {}

type Token = Number | Name | Plus | Minus;

Plus and Minus carry no fields at all — for them, the type is the whole message.

The array is annotated Token[7], which makes every element a Token, so one array holds all four member types side by side:

let tokens: Token[7] = [
    Name { text: "price" },
    Plus {},
    Number { value: 2 },
    Minus {},
    Name { text: "discount" },
    Plus {},
    Number { value: 1 }
];

Asking which member

Inside the loop, is sorts the tokens without unpacking any of them:

for token in tokens {
    if token is Number {
        numbers++;
    }
    if token is (Plus | Minus) {
        operators++;
    }
}

token is Number is true for the two numbers. Put a smaller sum in parentheses, token is (Plus | Minus), and the test is true when the active member is any one of them — the three operators here. It is the is form of a subset pattern.

The parentheses are required. Without them, token is Plus | Minus is refused, and the compiler's help shows the grouped form to write instead.

An ordinary bool

The result is a bool like any other, so it can be stored, printed or combined with && and ||:

let startsWithName = tokens[0] is Name;
PrintLine("starts with a name: {}", startsWithName);
PrintLine("ends with a number: {}", tokens[6] is Number);

What is does not do

is only asks. Two things follow from that.

It does not narrow. Inside if token is Number { … }, token is still the whole Token, not a Number. Reading token.value there is rejected, because a Name, a Plus or a Minus has no value. To use what is inside, bind it with a typed pattern in a match.

It does not quietly answer false. A test that could never be true is a mistake, and the compiler says so. token is bool is refused because bool is not a member of Token. On a value that is not a sum at all, is compares exact types, and a mismatch is an error too.

flowchart LR
    q{"Do you need the<br/>value inside?"} -- "no — only which member" --> is["`value is T`<br/>`value is (A | B)`"]
    q -- "yes" --> m["`match` with a typed pattern<br/>`n: Number => n.value`"]
You wantWriteYou get
to know the membertoken is Numbera bool
to know if it is one of severaltoken is (Plus | Minus)a bool
the value insidematch token { n: Number => … }n, a Number

The program

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

Src/Main.rux
// Sometimes a program only needs to know *which* member a sum holds, not what is inside it.
// `value is T` answers that with a plain `bool`: true when the active member is `T`. Put a smaller
// sum in parentheses, `value is (Plus | Minus)`, and it is true when the active member is any one
// of them. The parentheses are required: `token is Plus | Minus` is refused with
//       error: a sum type after 'is' must be grouped
//       help: write 'value is (Plus | Minus)'
//
// Two things `is` does not do:
//
// - It does not narrow. Inside `if token is Number { ... }`, `token` is still the whole sum, so
//   `token.value` is rejected:
//       error: type 'Minus | Name | Number | Plus' has no field 'value'
//   To use what is inside, bind it with a typed pattern in a `match`.
//
// - It does not quietly answer `false`. A test that could never be true is a mistake, and the
//   compiler says so. `token is bool` is refused because `bool` is not one of the members:
//       error: type 'bool8' is not a member or subset of sum 'Minus | Name | Number | Plus'
//   On a value that is not a sum at all, `is` compares exact types, and a mismatch is an error
//   too. The counter `numbers` is an `int`, so `numbers is bool` gives
//       error: 'is bool8' can never be true for a value of type 'int'
import Io::PrintLine;

// The pieces of the expression `price + 2 - discount + 1`, as a tokenizer might hand them over.
struct Number {
    value: int32;
}

struct Name {
    text: char8[..];
}

struct Plus {}

struct Minus {}

type Token = Number | Name | Plus | Minus;

func Main() -> int {
    // The annotation makes every element a `Token`, so one array holds all four member types.
    let tokens: Token[7] = [
        Name { text: "price" },
        Plus {},
        Number { value: 2 },
        Minus {},
        Name { text: "discount" },
        Plus {},
        Number { value: 1 }
    ];

    var numbers = 0;
    var operators = 0;
    for token in tokens {
        // `is` only asks; the token is untouched and keeps its type.
        if token is Number {
            numbers++;
        }
        if token is (Plus | Minus) {
            operators++;
        }
    }
    PrintLine("{} numbers, {} operators", numbers, operators);

    // The result is an ordinary boolean, so it can be stored and printed like one.
    let startsWithName = tokens[0] is Name;
    PrintLine("starts with a name: {}", startsWithName);
    PrintLine("ends with a number: {}", tokens[6] is Number);
    return 0;
}

Run it

cd Examples/SumTypes/Is
rux run
2 numbers, 3 operators
starts with a name: true
ends with a number: true

Common mistakes

Try it yourself

  1. Count the Name tokens as well, and print all three counts.
  2. Write func IsOperator(token: Token) -> bool with is, and use it in the loop.
  3. Add up the values of all the Number tokens. Why does this one need a match with an else => {} arm rather than is?

Learn more