Pattern Syntax

In this section, we explore the different kinds of patterns you can use in Oxide. Patterns are combinations of the above for matching against one or all values. Each type of pattern has its own use cases.

Literal Patterns

You can match against literal values directly:

fn handleValue(x: Int) {
    match x {
        1 -> println!("One"),
        2 -> println!("Two"),
        3 -> println!("Three"),
        _ -> println!("Other"),
    }
}

fn handleText(text: String) {
    match text {
        "hello" -> println!("Hello there!"),
        "goodbye" -> println!("See you!"),
        _ -> println!("Unknown greeting"),
    }
}

fn handleBoolean(b: Bool) {
    match b {
        true -> println!("It is true"),
        false -> println!("It is false"),
    }
}

Named Variable Patterns

A named variable pattern matches any value and binds the value to a variable:

fn printValue(value: Int) {
    // The pattern 'value' will match any Int
    // and the value is already bound to the parameter
    println!("Got: \(value)")
}

let x = 5      // pattern 'x' matches 5
let y = Some(3) // pattern 'y' matches Some(3)

In match expressions, variables capture the matched value:

fn processNumber(value: Int) {
    match value {
        0 -> println!("Zero"),
        n -> println!("Number: \(n)"),  // 'n' captures the value
    }
}

Wildcard Patterns in Match Arms

Use _ as the wildcard pattern when no other pattern matches:

fn ignoreValue(x: Int) {
    match x {
        1 -> println!("One"),
        2 -> println!("Two"),
        _ -> println!("Something else"),
    }
}

// Using _ in destructuring to ignore values
let (x, _, z) = (1, 2, 3)
// x is 1, the middle value is ignored, z is 3

// Using _ in let statements
let (first, _) = tuple

Use _ wherever you would normally use a wildcard within a pattern.

match point {
    (0, 0) -> println!("Origin"),
    (x, 0) -> println!("On x-axis at \(x)"),
    (0, y) -> println!("On y-axis at \(y)"),
    (_, _) -> println!("Somewhere else"),
}

Multiple Patterns with |

The | operator lets you match multiple patterns in a single arm:

fn describeNumber(n: Int) {
    match n {
        1 | 2 | 3 -> println!("One, two, or three"),
        4 | 5 | 6 -> println!("Four, five, or six"),
        _ -> println!("Something else"),
    }
}

fn describeVowel(c: char) {
    match c {
        'a' | 'e' | 'i' | 'o' | 'u' -> println!("Vowel"),
        _ -> println!("Consonant"),
    }
}

Range Patterns

You can use ranges to match multiple values:

fn describeNumber(n: Int) {
    match n {
        1..=5 -> println!("Between 1 and 5"),
        6..=10 -> println!("Between 6 and 10"),
        _ -> println!("Outside the range"),
    }
}

fn describeGrade(grade: char) {
    match grade {
        'a'..='z' -> println!("Lowercase letter"),
        'A'..='Z' -> println!("Uppercase letter"),
        '0'..='9' -> println!("Digit"),
        _ -> println!("Other character"),
    }
}

Range patterns are inclusive on both ends with ..=.

Destructuring Structs

You can destructure struct fields in patterns:

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

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

// In match expressions:
match point {
    Point { x: 0, y: 0 } -> println!("Origin"),
    Point { x, y: 0 } -> println!("On x-axis at \(x)"),
    Point { x: 0, y } -> println!("On y-axis at \(y)"),
    Point { x, y } -> println!("At (\(x), \(y))"),
}

You can also rename fields while destructuring:

match point {
    Point { x: horizontal, y: vertical } -> {
        println!("Horizontal: \(horizontal), Vertical: \(vertical)")
    },
    _ -> {},
}

And use .. to ignore remaining fields:

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

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

match user {
    User { name, age, .. } -> println!("\(name) is \(age) years old"),
}

Destructuring Enums

We've seen enum destructuring before, but let's review the full syntax:

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

fn processMessage(msg: Message) {
    match msg {
        // Unit variant
        Message.Quit -> println!("Quit"),

        // Struct-like variant
        Message.Move { x, y } -> println!("Moving to (\(x), \(y))"),

        // Tuple-like variant with single value
        Message.Write(text) -> println!("Writing: \(text)"),

        // Tuple-like variant with multiple values
        Message.ChangeColor(r, g, b) -> println!("RGB(\(r), \(g), \(b))"),
    }
}

Destructuring Tuples

Tuples can be destructured to extract individual values:

fn printTuple((x, y): (Int, String)) {
    println!("x: \(x), y: \(y)")
}

// In match expressions:
let point = (1, 2, 3)
match point {
    (0, 0, 0) -> println!("Origin"),
    (x, 0, 0) -> println!("On x-axis"),
    (x, y, z) -> println!("Point: (\(x), \(y), \(z))"),
}

You can use _ to ignore values:

let (x, _) = tuple
let (first, _, third) = tuple

Nested Patterns

Patterns can be nested for destructuring complex data:

enum Color {
    Rgb(Int, Int, Int),
    Hsv(Int, Int, Int),
}

enum Message {
    Quit,
    ChangeColor(Color),
}

fn processMessage(msg: Message) {
    match msg {
        Message.ChangeColor(Color.Rgb(r, g, b)) -> {
            println!("RGB: \(r), \(g), \(b)")
        },
        Message.ChangeColor(Color.Hsv(h, s, v)) -> {
            println!("HSV: \(h), \(s), \(v)")
        },
        Message.Quit -> println!("Quit"),
    }
}

// Nested tuple destructuring:
let ((x, y), (a, b)) = ((1, 2), (3, 4))
println!("x: \(x), y: \(y), a: \(a), b: \(b)")

Match Guards

A match guard is an additional if condition specified after the pattern in a match arm that must also be true for that arm to be chosen:

fn checkNumber(n: Int?) {
    match n {
        Some(x) if x < 0 -> println!("Negative: \(x)"),
        Some(x) if x == 0 -> println!("Zero"),
        Some(x) -> println!("Positive: \(x)"),
        null -> println!("No value"),
    }
}

fn processUser(user: User) {
    match user {
        User { age, .. } if age >= 18 -> println!("Adult"),
        User { age, .. } if age > 0 -> println!("Minor"),
        _ -> println!("Invalid age"),
    }
}

Match guards are useful when you need to express conditions that patterns alone cannot express:

fn classifyNumber(n: Int) {
    match n {
        n if n % 2 == 0 -> println!("Even"),
        n if n % 2 != 0 -> println!("Odd"),
        _ -> println!("Not a number"),
    }
}

You can use complex conditions in guards:

fn processValue(value: Int, max: Int) {
    match value {
        v if v > 0 && v < max -> println!("In range"),
        v if v == max -> println!("At max"),
        v if v < 0 -> println!("Negative"),
        _ -> println!("Out of range"),
    }
}

Binding with @ Pattern

The @ operator lets you bind a value while also matching against a pattern:

fn checkRange(num: Int) {
    match num {
        n @ 1..=5 -> println!("Small number: \(n)"),
        n @ 6..=10 -> println!("Medium number: \(n)"),
        n -> println!("Large number: \(n)"),
    }
}

enum Message {
    Hello { id: Int },
}

fn processMessage(msg: Message) {
    match msg {
        Message.Hello { id: id @ 5..=7 } -> {
            println!("Hello with special ID: \(id)")
        },
        Message.Hello { id } -> println!("Hello with ID: \(id)"),
    }
}

Practical Examples

Complex Configuration Matching

struct Config {
    port: Int,
    host: String,
    tls: Bool?,
}

fn setupServer(config: Config) {
    match config {
        Config { port: 80, host, tls: null } -> {
            println!("HTTP server on \(host):80")
        },
        Config { port: 443, host, tls: true } -> {
            println!("HTTPS server on \(host):443")
        },
        Config { port, host, tls } -> {
            println!("Server on \(host):\(port)")
        },
    }
}

Processing Nested Data

struct Address {
    street: String,
    city: String,
    country: String,
}

struct Person {
    name: String,
    address: Address?,
}

fn printLocation(person: Person) {
    match person {
        Person {
            name,
            address: Some(Address { city, country, .. }),
        } -> println!("\(name) lives in \(city), \(country)"),
        Person { name, address: null } -> println!("\(name) has no address"),
    }
}

Handling Multiple Enum Variants

enum Result {
    Success(String),
    Error(String),
    Pending,
}

fn processResult(result: Result) {
    match result {
        Result.Success(msg) | Result.Pending -> {
            println!("Good state: \(msg)")
        },
        Result.Error(err) -> println!("Error: \(err)"),
    }
}

Pattern syntax in Oxide is extremely powerful and expressive. By mastering these various pattern forms, you can write code that is both safe and concise, with the compiler ensuring that you handle all cases correctly.

Advanced Pattern Techniques

Now that you understand the basics of patterns, let's explore some more advanced techniques that will help you write cleaner, more expressive Oxide code.

Guard let: Conditionally Unwrapping in Guards

The guard let construct combines pattern matching with conditional logic, allowing you to unwrap a value and check a condition in a single statement. This is particularly useful at the beginning of functions to handle error cases early.

Basic guard let Syntax

fn processOptionalNumber(value: Int?) {
    guard let num = value else {
        println!("No value provided")
        return
    }

    println!("Got number: \(num)")
    println!("Double: \(num * 2)")
}

The guard let statement can be read as: "Guard against the case where this pattern doesn't match." If the pattern doesn't match, the else block executes and typically returns early.

guard let vs if let

guard let and if let both unwrap nullable types, but they're used in different situations:

// Use if let when you want to handle just the success case
if let user = findUser(id) {
    displayUser(user)
}

// Use guard let when you need to handle the failure case first
fn processUser(userId: Int) {
    guard let user = findUser(userId) else {
        println!("User not found")
        return
    }

    // Now user is guaranteed to be unwrapped for the rest of the function
    println!("Processing: \(user.name)")
    updateUserStatus(user)
    sendNotification(user)
}

Multiple guard let Statements

You can chain multiple guard let statements to handle several optional values:

fn setupConnection(host: String?, port: Int?, credentials: String?) {
    guard let h = host else {
        println!("Host is required")
        return
    }

    guard let p = port else {
        println!("Port is required")
        return
    }

    guard let creds = credentials else {
        println!("Credentials are required")
        return
    }

    println!("Connecting to \(h):\(p) with provided credentials")
    connect(h, p, creds)
}

Or more concisely with a single guard statement:

fn setupConnection(host: String?, port: Int?, credentials: String?) {
    guard let h = host && let p = port && let creds = credentials else {
        println!("Host, port, and credentials are required")
        return
    }

    println!("Connecting to \(h):\(p)")
    connect(h, p, creds)
}

guard let with Conditions

You can add conditions to guard let for more complex validation:

fn validateUser(user: User?) {
    guard let u = user && u.isActive else {
        println!("User is not active")
        return
    }

    println!("User \(u.name) is active and ready")
}

fn processPayment(amount: Int?) {
    guard let amt = amount && amt > 0 && amt < 1000000 else {
        println!("Invalid amount")
        return
    }

    println!("Processing payment of \(amt)")
}

guard let in Different Contexts

In Function Bodies

fn findAndProcessUser(userId: Int): String? {
    guard let user = fetchUserFromDatabase(userId) else {
        return "User not found"
    }

    updateLastSeen(user)
    return "Processing \(user.name)"
}

In Method Bodies

struct DataProcessor {
    fn processData(input: String?) {
        guard let data = input else {
            println!("No input data")
            return
        }

        let processed = transform(data)
        save(processed)
    }
}

In Closure Bodies

let users: Vec<User> = vec![]

// Using guard let in a closure
users.forEach { user ->
    guard let profile = user.profile else {
        println!("Skipping user without profile")
        return
    }

    displayProfile(profile)
}

Combining Patterns with Multiple Conditions

You can create complex pattern matching scenarios by combining multiple features:

Multiple Conditions with Guards

fn categorizeRequest(request: Request) {
    match request {
        Request { method: "GET", path, .. } if path.starts(with: "/api") -> {
            println!("API GET request for \(path)")
        },
        Request { method: "POST", path, .. } if path.starts(with: "/api") -> {
            println!("API POST request for \(path)")
        },
        Request { method: m, path: p, .. } if p.contains("health") -> {
            println!("Health check: \(m) \(p)")
        },
        _ -> println!("Other request"),
    }
}

Combining Multiple Pattern Types

enum NetworkEvent {
    Connected(Int),
    Disconnected(String),
    DataReceived(String),
    Error(String),
}

fn handleNetworkEvent(event: NetworkEvent) {
    match event {
        // Binding and range pattern
        NetworkEvent.Connected(port) if port >= 1024 && port <= 65535 -> {
            println!("Connected on valid port \(port)")
        },

        // Destructuring with condition
        NetworkEvent.Error(msg) if msg.contains("timeout") -> {
            println!("Timeout error: \(msg)")
        },

        // Multiple patterns
        NetworkEvent.DataReceived(data) | NetworkEvent.Connected(_) -> {
            println!("Received something")
        },

        _ -> println!("Other event"),
    }
}

Pattern Refining Strategy

When writing complex patterns, use this strategy to keep code readable:

1. Start with the Most Specific Patterns

// Good: specific patterns first
match status {
    Status.Success(code) if code == 200 -> handleSuccess(),
    Status.Success(code) if code >= 300 && code < 400 -> handleRedirect(),
    Status.Error(msg) if msg.contains("timeout") -> handleTimeout(),
    Status.Error(msg) -> handleError(msg),
    _ -> handleUnknown(),
}

// Bad: general patterns might catch specific cases
match status {
    _ -> handleAny(),  // This would prevent other patterns from executing
    Status.Success -> handleSuccess(),  // Unreachable!
}
// Group by functionality
match data {
    // All success cases
    ParseResult.Json(obj) | ParseResult.Xml(obj) -> {
        processObject(obj)
    },

    // All error cases
    ParseResult.InvalidFormat(err) | ParseResult.DecodeError(err) -> {
        logError(err)
    },

    // Default
    ParseResult.Empty -> println!("No data"),
}

3. Use Helper Functions for Complex Patterns

fn isValidEmail(email: String): Bool {
    email.contains("@") && email.contains(".")
}

fn processUser(user: User) {
    match user {
        User { email, .. } if isValidEmail(email) -> {
            sendWelcome(user)
        },
        User { email, .. } -> {
            println!("Invalid email: \(email)")
        },
    }
}

Practical Examples

Configuration Validation with guard let

struct AppConfig {
    databaseUrl: String?,
    apiKey: String?,
    debugMode: Bool,
}

fn startApp(config: AppConfig?) {
    guard let cfg = config else {
        println!("Configuration is required")
        return
    }

    guard let dbUrl = cfg.databaseUrl else {
        println!("Database URL is required")
        return
    }

    guard let apiKey = cfg.apiKey else {
        println!("API key is required")
        return
    }

    println!("Starting app with DB: \(dbUrl)")
    println!("Debug mode: \(cfg.debugMode)")

    initializeDatabase(dbUrl)
    setApiKey(apiKey)
}

Type-Safe API Response Handling

enum ApiResponse {
    Success(String),
    Failure(Int, String),
    NetworkError(String),
}

fn processApiResponse(response: ApiResponse) {
    match response {
        ApiResponse.Success(data) -> {
            println!("Success: \(data)")
        },
        ApiResponse.Failure(code, message) if code >= 400 && code < 500 -> {
            println!("Client error \(code): \(message)")
        },
        ApiResponse.Failure(code, message) if code >= 500 -> {
            println!("Server error \(code): \(message)")
            retryRequest()
        },
        ApiResponse.NetworkError(err) -> {
            println!("Network error: \(err)")
            retryRequest()
        },
        _ -> println!("Unknown response"),
    }
}

Data Extraction with Nested Patterns and Guards

struct Message {
    from: String,
    to: String?,
    content: String,
    attachments: Vec<String>,
}

fn processEmail(message: Message) {
    guard let recipient = message.to else {
        println!("Message has no recipient")
        return
    }

    match message {
        Message { content, attachments, .. }
            if attachments.count > 0 && content.contains("invoice") -> {
            println!("Processing invoice with attachments")
            processInvoice(content, attachments)
        },

        Message { content, .. } if content.starts(with: "URGENT:") -> {
            println!("Urgent message to \(recipient)")
            markAsUrgent()
        },

        Message { from, content, .. } -> {
            println!("Regular message from \(from)")
            saveToArchive(content)
        },
    }
}

Best Practices for Advanced Patterns

  1. Use guard let for early returns - It makes your intent clear and improves readability
  2. Put the most specific patterns first - Ensures they get evaluated before catch-all patterns
  3. Use guards for additional conditions - When pattern matching alone isn't expressive enough
  4. Group related patterns with | - Reduces repetition and groups similar logic
  5. Keep patterns readable - Use helper functions if patterns become too complex
  6. Prefer exhaustive matching - Use match instead of if let when handling multiple cases

By mastering these advanced techniques, you'll be able to write Oxide code that is both powerful and maintainable, with the compiler ensuring that you handle all cases correctly.