Patterns and Matching

Tim Heaney

Agenda

18. Patterns and Matching

  • 18.0. Review
  • 18.1. All the Places Patterns Can Be Used
  • 18.2. Refutability: Whether a Pattern Might Fail to Match
  • 18.3. Pattern Syntax

18.0 Review

Pattern Matching

  • We saw some already in chapter 6: Enums and Pattern Matching
  • To imperative programmers, pattern matching might seem weird
  • Functional programmers (OCaml, Elixir, F#, &c.) will feel more at home

Recall chapter 6, Enums and Pattern Matching

match

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

Enum with value

enum UsState {
    Alabama,
    Alaska,
    ..
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

Match and bind to value

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}!", state);
            25
        }
    }
}

regular quarter

regular-quarter.jpg

state quarter

north-dakota-quarter.jpg

But we still have regular quarters…

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(Option<UsState>),
}

bicentennial quarter

bicentennial-quarter.jpg

model American change

enum Series {
    Regular,
    Bicentennial,
    UsStates(UsState),
    UsTerritories(UsTerritory),
    AmericaTheBeautiful(NationalPark),
    AmericanWomen(Woman),
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(Series),
}

Match an Option

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

Match with if let

fn plus_one(x: Option<i32>) -> Option<i32> {
    if let Some(i) = x {
        return Some(i + 1);
    }
    None
}

18.1 All the Places Patterns Can Be Used

match arms

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

conditional if let expressions

if let PATTERN = VALUE {
    ...
}

Note: if let is not exhaustive, but can have an else clause

if let PATTERN = VALUE {
    ...
} else {
    ...
}

while let

while let PATTERN = VALUE {
    ...
}

While let example

while let Some(top) = stack.pop() {
    println!("{top}");
}

When the match fails, the loop ends.

for loops

for PATTERN in ITERATOR {
    ...
}

For loop example

let v = vec!['a', 'b', 'c'];

for (index, value) in v.iter().enumerate() {
    println!("{value} is at index {index}");
}

let statements

let PATTERN = VALUE;

Let statement example

let t = (1, 2, 3);

let (x, y, z) = t;

println!("x: {x}, y: {y}, z: {z}");

Error if it doesn't match

let t = (1, 2, 3);

let (x, y) = t;

println!("x: {x}, y: {y}");

Compilation error, of course

❯ cargo run
   Compiling pattern-matching v0.1.0 (/home/tim/rust/pattern-matching)
error[E0308]: mismatched types
  --> src/main.rs:19:9
   |
19 |     let (x, y) = t;
   |         ^^^^^^   - this expression has type `({integer}, {integer}, {integer})`
   |         |
   |         expected a tuple with 3 elements, found one with 2 elements
   |
   = note: expected tuple `({integer}, {integer}, {integer})`
              found tuple `(_, _)`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `pattern-matching` due to previous error

Assign to underscore so match succeeds

let t = (1, 2, 3);

let (x, y, _) = t;

println!("x: {x}, y: {y}");

Function parameters

fn name(PATTERN: TYPE) {
    ...
}

Function parameters example

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({x}, {y})");
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}

Closures

let print_coordinates = |&(x, y):  &(i32, i32)| {
    println!("Current location: ({x}, {y})");
};

Assignments

As of Rust 1.59, we can use tuple, slice, and struct patterns as the left-hand side of an assignment.

let (a, b, c, d, e);

(a, b) = (1, 2);
[c, .., d, _] = [1, 2, 3, 4, 5];
Struct { e, .. } = Struct { e: 5, f: 3 };

assert_eq!([1, 2, 1, 4, 5], [a, b, c, d, e]);

18.2 Refutability: Whether a Pattern Might Fail to Match

Patterns come in two forms

  • irrefutable
let x = 5;
  • refutable
if let Some(x) = y {
    println!("{x}");
}

Irrefutable

  • Function parameters
  • Let statements
  • For loops

Meant to be refutable

  • if let
  • while let

Improperly using a refutable pattern is an error

let Some(x) = y;
error[E0005]: refutable pattern in local binding: `None` not covered
   --> src/main.rs:41:9
    |
41  |     let Some(x) = y;
    |         ^^^^^^^ pattern `None` not covered
    |
...

Improperly using an irrefutable pattern is a warning

if let x = 5 {
    println!("{x}");
}
warning: irrefutable `if let` pattern
  --> src/main.rs:37:8
   |
37 |     if let x = 5 {
   |        ^^^^^^^^^
   |
...

Match arms

  • each arm must use a refutable pattern
  • except last arm, which is irrefutable
  • (last arm matches everything else)

18.3 Pattern Syntax

matching literals

let x = 1;

match x {
    1 => println!("one"),
    2 => println!("two"),
    3 => println!("three"),
    _ => println!("anything"),
}

matching named variables

let x = Some(5);
let y = 10;

match x {
    Some(50) => println!("Got 50"),
    Some(y) => println!("Matched, y = {y}"),
    _ => println!("Default case, x = {x:?}"),
}

println!("at the end: x = {x:?}, y = {y}");

matching multiple patterns

use | for or

let x = 1;

match x {
    1 | 2 => println!("one or two"),
    3 => println!("three"),
    _ => println!("anything"),
}

matching ranges

let x = 5;

match x {
    1..=5 => println!("one through five"),
    _ => println!("something else"),
}

Nicer than 1 | 2 | 3 | 4 | 5

ranges with characters

let x = 'c';

match x {
    'a'..='j' => println!("early ASCII letter"),
    'k'..='z' => println!("late ASCII letter"),
    _ => println!("something else"),
}
  • Ranges are only allowed with numeric values or char values

Destructuring

  • structs
  • enums
  • tuples
  • references

Destructuring structs

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x: a, y: b } = p;
    assert_eq!(0, a);
    assert_eq!(7, b);
}

Shorthand

  • When the variable names match the field names, we don't have to write the field names
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x, y } = p;
    assert_eq!(0, x);
    assert_eq!(7, y);
}

Destructuring enums

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

Destructuring enums 2

fn main() {
    let msg = Message::ChangeColor(0, 160, 255);
...

Destructuring enums 3

    match msg {
        Message::Quit => {
            println!("No data to destructure.")
        }
        Message::Move { x, y } => {
            println!("Move x {x} and y {y}")
        }
        Message::Write(text) => {
            println!("Text message: {text}")
        }
        Message::ChangeColor(r, g, b) => {
            println!("red {r}, green {g}, and blue {b}")
        }
    }
}

Destructuting nested structs and enums

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(Series::UsStates(state)) => {
            println!("State quarter from {:?}!", state);
            25
        },
        Coin::Quarter(_) => 25,
    }
}

Destructuring tuples

let t = (1, 2, 3);

let (x, y, z) = t;

println!("x: {x}, y: {y}, z: {z}");

Destructuring references

let reference = &3;

let &value = reference;

println!("value: {value}");

Same as this

let reference = &3;

let value = *reference;

println!("value: {value}");

Ignoring values in a pattern

  • Ignoring an entire value with underscore _
  • Ignoring parts of a value with a nested _
  • Ignoring an unused variable by starting its name with _
  • Ignoring remaining parts of a value with ..

Ignoring an entire value with underscore _

fn foo(_: i32, y: i32) {
    println!("y parameter: {y}");
}

Ignoring parts of a value with a nested _

let t = (1, 2, 3);

match t {
    (1, y, _) => println!("x: 1, y: {y}"),
    _ => println!("no match"),
}

Ignoring an unused variable by starting its name with _

fn main() {
    let _x = 5; // no unused warning
    let y = 10; // unused variable warning
}

But it still binds the variable!

This won't compile (s is moved into _s)

let s = Some(String::from("Hello!"));

if let Some(_s) = s { 
    println!("found a string");
}

println!("{:?}", s);

Underscore does not bind

This works fine!

let s = Some(String::from("Hello!"));

if let Some(_) = s {
    println!("found a string");
}

println!("{:?}", s);

Ignoring remaining parts of a value with ..

let t = (1, 2, 3);

let (x, ..) = t;

println!("x: {x}");

Match Guards

let num = Some(4);

match num {
    Some(x) if x < 5 => println!("less than five: {x}"),
    Some(x) => println!("{x}"),
    None => (),
}

@ Bindings

If we have this enum defined…

enum Message {
    Hello { id: i32 },
}

…then this will print

"Found an id in range: 5"

let msg = Message::Hello { id: 5 };

match msg {
    Message::Hello { id: id_value @ 3..=7 } => {
        println!("Found an id in range: {id_value}")
    },
    Message::Hello { id: 10..=12 } => {
        println!("Found an id in another range")
    }
    Message::Hello { id } => {
        println!("Found some other id: {id}")
    }
}

That last one

Message::Hello { id } => {
    println!("Found some other id: {id}")
}

is short-hand for

Message::Hello { id: id } => {
    println!("Found some other id: {id}")
}

More

Thanks!

tim.jpg