All the Places Patterns Can Be Used

We've seen patterns used in several places in the previous chapters. This section explores all the places where patterns can appear in Oxide and how to use them effectively.

match Expressions

As we discussed in the "Enums and Pattern Matching" chapter, match expressions use patterns in their arms. The syntax is:

match VALUE {
    PATTERN1 -> EXPRESSION1,
    PATTERN2 -> EXPRESSION2,
    PATTERN3 -> EXPRESSION3,
}

One requirement of match expressions is that they must be exhaustive, meaning every possible value of the type being matched must be covered. A good way to ensure this is to have a catch-all pattern as the last arm:

fn describeValue(value: Int?) {
    match value {
        Some(n) if n > 0 -> println!("Positive: \(n)"),
        Some(n) -> println!("Non-positive: \(n)"),
        null -> println!("No value"),
    }
}

Conditional if let Expressions

As we discussed in the "if let and while let" chapter, if let is a concise way to match one pattern while ignoring the rest. The syntax is:

if let PATTERN = EXPRESSION {
    // code that runs if the pattern matches
} else {
    // optional else block
}

The if let construct is less strict than match because it doesn't require exhaustive pattern matching. You can use it when you only care about one specific pattern:

let coin = Coin.Quarter(UsState.Alaska)

if let Coin.Quarter(state) = coin {
    println!("State quarter from \(state:?)")
}

One advantage of if let is that it's more concise when dealing with nullable types, thanks to Oxide's auto-unwrap feature:

let maybeValue: Int? = Some(5)

if let value = maybeValue {
    println!("The value is: \(value)")
}

while let Loops

The while let construct allows a loop to run as long as a pattern continues to match. This is useful when working with iterators or sequences that return nullable values:

var numbers: Vec<Int> = vec![1, 2, 3]

while let Some(num) = numbers.pop() {
    println!("Popped: \(num)")
}

Or with auto-unwrap:

var stack: Vec<Int> = vec![1, 2, 3]

while let value = stack.pop() {
    println!("Got: \(value)")
}

Function Parameters

Patterns can be used in function parameters, allowing you to destructure arguments directly:

fn printPoint(point: (Int, Int)) {
    let (x, y) = point
    println!("Point is at x=\(x), y=\(y)")
}

But you can also destructure directly in the function signature:

fn printPoint((x, y): (Int, Int)) {
    println!("Point is at x=\(x), y=\(y)")
}

This works with structs too:

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

fn printPoint(Point { x, y }: Point) {
    println!("Point is at x=\(x), y=\(y)")
}

let Statements

Every let statement you've written uses patterns:

let x = 5  // matches the pattern 'x'
let (x, y, z) = (1, 2, 3)  // destructuring tuple
let Point { x, y } = point  // destructuring struct

The pattern comes after let. In the simplest case (let x = 5), the pattern is just a variable name.

You can also use patterns to destructure more complex values:

struct User {
    name: String,
    email: String,
    age: Int,
}

let user = User {
    name: "Alice".toString(),
    email: "alice@example.com".toString(),
    age: 30,
}

// Destructure the struct
let User { name, email, age } = user
println!("User: \(name), Email: \(email), Age: \(age)")

// Or rename fields while destructuring
let User { name: userName, email, age } = user
println!("Name: \(userName)")

Pattern Syntax in Practice

Ignoring Values in let Statements

Sometimes you want to bind only some values from a destructuring:

let (x, _, z) = (1, 2, 3)
// x is 1, z is 3, we ignore the middle value

Or using _ to ignore:

let (x, _) = (1, 2)
// x is 1, we ignore the second value

Ignoring Remaining Values

You can use .. to ignore remaining values in a destructuring:

struct Point {
    x: Int,
    y: Int,
    z: Int,
}

let Point { x, .. } = point
// x is bound, y and z are ignored

This is particularly useful with larger structs where you only need a few fields:

struct Config {
    host: String,
    port: Int,
    username: String,
    password: String,
    timeout: Int,
    retries: Int,
}

let Config { host, port, .. } = config
println!("Connecting to \(host):\(port)")

Practical Examples

Processing Command Line Arguments

fn processArgs(args: Vec<String>) {
    match args.count() {
        0 -> println!("No arguments"),
        1 -> println!("One argument: \(args[0])"),
        2 -> {
            let [first, second] = [args[0], args[1]]
            println!("Two args: \(first) and \(second)")
        },
        _ -> println!("Many arguments: \(args.count())"),
    }
}

Parsing Configuration Files

enum ConfigValue {
    String(String),
    Number(Int),
    Boolean(Bool),
    List(Vec<ConfigValue>),
}

fn printConfigValue(value: ConfigValue) {
    match value {
        ConfigValue.String(s) -> println!("String: \(s)"),
        ConfigValue.Number(n) -> println!("Number: \(n)"),
        ConfigValue.Boolean(b) -> println!("Boolean: \(b)"),
        ConfigValue.List(items) -> println!("List with \(items.count()) items"),
    }
}

Working with Results and Options

fn processFile(path: String) {
    if let content = readFile(path) {
        if let lines = content.split("\n") {
            for line in lines {
                println!("Line: \(line)")
            }
        }
    } else {
        println!("Failed to read file")
    }
}

Patterns are a fundamental part of Oxide's expressiveness. They allow you to extract values from complex data structures and ensure that your code handles all cases correctly. By understanding where and how patterns can be used, you'll write more powerful and concise Oxide code.