Patterns · Lesson 7.5

Character pattern

Source
Match character literals and escapes such as 'w' and '\n', and a whole class of characters with a range such as 'a'..='z'.
You'll need: Character, Match, Guard

A character literal is a pattern, just as a number is. That makes match the natural way to react to a key press, a command letter or a separator in some text: one arm per character, and else for the rest. This lesson shows the arms, the one rule about their type, and how to match a whole class of characters such as "any digit".

One arm per character

'w' => matches the character w. Escapes work the same way they do in a literal: ' ' is a space and '\n' a line break, and any Unicode character can have an arm, 'λ' included.

func Command(key: char32) -> char8[..] {
    return match key {
        'w' => "move up",
        'a' => "move left",
        's' => "move down",
        'd' => "move right",
        ' ' => "jump",
        'λ' => "cast a spell",
        else => "ignored"
    };
}

Uppercase and lowercase are different characters, so 'W' does not reach the first arm. Anything without an arm of its own falls through to else. A match that produces a value from a character should always end with one: a char32 has far too many values to list, and else is where you decide what all the others mean.

The pattern and the value share a width

A pattern must have the same character width as the value it is matched against:

Value typePatternExample
char32 — an unprefixed literal's type'w''w' =>
char8 — such as a byte out of a stringc8'w'c8'w' =>

Command takes a char32, so its plain 'w' patterns fit. A char8 value is matched with the c8 prefix you met in Character — c8'w'. Every byte of a string literal is a char8, as String literal shows later in the course.

A class of characters is a range

A range of characters is a pattern, just as a range of numbers is: 'a'..='z' matches every lowercase ASCII letter.

func Kind(key: char32) -> char8[..] {
    return match key {
        '\n' => "line break",
        '0'..='9' => "digit",
        'a'..='z' => "lowercase letter",
        else => "something else"
    };
}

This works because characters are ordered by their code, and the digits, like the letters a to z, have consecutive codes. The bounds follow the width rule above, so a char8 value is matched with c8'a'..=c8'z'. A class that no single range spells, such as "a vowel", is still a job for a guard. '7' lies between '0' and '9'; 'K' is an uppercase letter, whose codes come before the lowercase ones, so it is "something else".

flowchart LR
    k["`key`"] --> lf{"a line break?"}
    lf -- "yes" --> l1["line break"]
    lf -- "no" --> d{"`'0' ≤ c ≤ '9'?`"}
    d -- "yes" --> l2["digit"]
    d -- "no" --> a{"`'a' ≤ c ≤ 'z'?`"}
    a -- "yes" --> l3["lowercase letter"]
    a -- "no" --> l4["something else"]

The program

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

Src/Main.rux
// A character literal is a pattern, just as a number is. `'w' =>` matches the character w, and
// escapes work the same way they do in a literal: `' '` is a space, `'\n'` a line break.
//
// The pattern must have the same character width as the value. An unprefixed `'w'` is a `char32`,
// so it matches a `char32` value; a `char8` value, such as a byte read out of a text literal, is
// matched with `c8'w'` instead. Mix the two and the compiler stops with
//     error: pattern has type 'char32', but the matched value has type 'char8'
//
// A range of characters is a pattern too: `'a'..='z' =>` matches every lowercase ASCII letter. Its
// bounds follow the same width rule, so a `char8` value is matched with `c8'a'..=c8'z'`. A class that
// no single range spells is still a guard's job.
import Io::PrintLine;

// One arm per key. Uppercase and lowercase are different characters, so `'W'` would not reach the
// first arm, and anything without an arm of its own falls through to `else`.
func Command(key: char32) -> char8[..] {
    return match key {
        'w' => "move up",
        'a' => "move left",
        's' => "move down",
        'd' => "move right",
        ' ' => "jump",
        'λ' => "cast a spell",
        else => "ignored"
    };
}

// A range pattern covers a whole class at once. Characters are ordered by their code, and the
// digits, like the letters, have consecutive codes.
func Kind(key: char32) -> char8[..] {
    return match key {
        '\n' => "line break",
        '0'..='9' => "digit",
        'a'..='z' => "lowercase letter",
        else => "something else"
    };
}

func Main() -> int {
    let keys: char32[6] = ['w', 'd', ' ', 'λ', 'W', 'q'];
    for index in 0..6 {
        PrintLine("'{}' -> {}", keys[index], Command(keys[index]));
    }

    PrintLine("'7' is a {}, 'k' is a {}, 'K' is {}", Kind('7'), Kind('k'), Kind('K'));
    PrintLine("'\\n' is a {}", Kind('\n'));
    return 0;
}

Run it

cd Examples/Patterns/CharacterPattern
rux run
'w' -> move up
'd' -> move right
' ' -> jump
'λ' -> cast a spell
'W' -> ignored
'q' -> ignored
'7' is a digit, 'k' is a lowercase letter, 'K' is something else
'\n' is a line break

Common mistakes

Try it yourself

  1. Make Command accept the uppercase keys W, A, S and D as well.
  2. Extend Kind so that 'K' is reported as an "uppercase letter".
  3. Loop over the text "pattern matching" with for c in … — each c is a char8 — and count its vowels with arms such as c8'a' =>.

Learn more

  • char32, char8 and match in the Rux Reference
  • Character — character literals, escapes and widths
  • Word count — a checkpoint project that sorts characters into letters and the rest