if let and while let

The if let syntax lets you combine if and let into a less verbose way to handle values that match one pattern while ignoring the rest. Consider the following program that matches on a Int? value in the config_max variable but only wants to execute code if the value is a Some variant:

let configMax: Int? = Some(3)

match configMax {
    Some(max) -> println!("The maximum is configured to be \(max)"),
    null -> {},
}

If the value is Some, we print out the value in the Some variant by binding the value to the variable max in the pattern. We don't want to do anything with the null value. To satisfy the match expression, we have to add null -> {} after processing just one variant, which is annoying boilerplate code to add.

Instead, we could write this in a shorter way using if let. The following code behaves the same as the match above:

let configMax: Int? = Some(3)

if let Some(max) = configMax {
    println!("The maximum is configured to be \(max)")
}

The syntax if let takes a pattern and an expression separated by an equal sign. It works the same way as a match, where the expression is given to the match and the pattern is its first arm. In this case, the pattern is Some(max), and the max binds to the value inside the Some. We can then use max in the body of the if let block in the same way we used max in the corresponding match arm. The code in the if let block isn't run if the value doesn't match the pattern.

Using if let means less typing, less indentation, and less boilerplate code. However, you lose the exhaustive checking that match enforces. Choosing between match and if let depends on what you're doing in your particular situation and whether gaining conciseness is an appropriate trade-off for losing exhaustive checking.

In other words, you can think of if let as syntax sugar for a match that runs code when the value matches one pattern and then ignores all other values.

Auto-Unwrapping for Nullable Types

Oxide provides a powerful feature for working with nullable types: automatic unwrapping in if let expressions. When the right-hand side of an if let is a nullable type (T?), you don't need to explicitly write Some(...) in your pattern. Oxide will automatically unwrap the value for you.

Consider this code:

let maybeUser: User? = findUser(id)

// Traditional way with explicit Some
if let Some(user) = maybeUser {
    println!("Hello, \(user.name)")
}

// Oxide auto-unwrap - simpler and more readable
if let user = maybeUser {
    println!("Hello, \(user.name)")
}

Both forms are equivalent and compile to the same code. The auto-unwrap syntax (if let user = maybeUser) is more concise and reads naturally: "if there's a user, use it."

This is particularly useful when working with function return values:

fn findUserById(id: Int): User? {
    // ... lookup logic
    null
}

fn processUser(id: Int) {
    if let user = findUserById(id) {
        println!("Found user: \(user.name)")
        sendWelcomeEmail(user)
    }
}

The auto-unwrap also works with chained method calls:

if let email = user.profile?.email {
    sendNotification(email)
}

Using else with if let

We can include an else with an if let. The block of code that goes with the else is the same as the block of code that would go with the null case in the match expression:

let coin = Coin.Quarter(UsState.Alaska)

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

This is equivalent to:

match coin {
    Coin.Quarter(state) -> println!("State quarter from \(state:?)"),
    else -> println!("Not a quarter"),
}

Combining if let with else if

You can chain if let expressions with else if and else if let:

fn describeValue(value: Int?) {
    if let n = value {
        if n > 100 {
            println!("Large number: \(n)")
        } else if n > 0 {
            println!("Positive number: \(n)")
        } else if n < 0 {
            println!("Negative number: \(n)")
        } else {
            println!("Zero")
        }
    } else {
        println!("No value")
    }
}

Or matching against multiple nullable types:

fn processCoordinates(x: Int?, y: Int?) {
    if let xVal = x {
        if let yVal = y {
            println!("Point: (\(xVal), \(yVal))")
        } else {
            println!("Only X coordinate: \(xVal)")
        }
    } else if let yVal = y {
        println!("Only Y coordinate: \(yVal)")
    } else {
        println!("No coordinates")
    }
}

if let with Conditions

You can combine if let with additional conditions using &&:

let user: User? = findUser(id)

if let user = user && user.isActive {
    greet(user)
}

This is equivalent to:

if let Some(user) = user {
    if user.isActive {
        greet(user)
    }
}

The combined syntax is more concise and clearly expresses the intent: "if we have a user AND they are active, greet them."

while let

Similar to if let, Oxide provides while let for looping as long as a pattern continues to match. This is particularly useful when working with iterators or any sequence that returns nullable values.

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

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

This code pops values from the stack and prints them until the stack is empty. The pop method returns Int?, returning Some(value) when there's a value and null when the stack is empty.

With auto-unwrap syntax:

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

while let top = stack.pop() {
    println!("Popped: \(top)")
}

Practical Examples

Working with Configuration

struct Config {
    databaseUrl: String?,
    maxConnections: Int?,
    timeout: Int?,
}

fn loadConfig(): Config {
    Config {
        databaseUrl: Some("postgres://localhost/db".toString()),
        maxConnections: Some(10),
        timeout: null,
    }
}

fn initializeDatabase() {
    let config = loadConfig()

    if let url = config.databaseUrl {
        println!("Connecting to: \(url)")

        let connections = config.maxConnections ?? 5
        println!("Max connections: \(connections)")

        if let timeout = config.timeout {
            println!("Timeout: \(timeout)s")
        } else {
            println!("No timeout configured, using default")
        }
    } else {
        println!("No database URL configured!")
    }
}
struct SearchResult {
    title: String,
    url: String,
    snippet: String?,
}

fn search(query: str): SearchResult? {
    // ... search logic
    Some(SearchResult {
        title: "Example".toString(),
        url: "https://example.com".toString(),
        snippet: Some("An example result".toString()),
    })
}

fn displaySearchResult(query: str) {
    if let result = search(query) {
        println!("Found: \(result.title)")
        println!("URL: \(result.url)")

        if let snippet = result.snippet {
            println!("Snippet: \(snippet)")
        }
    } else {
        println!("No results found for '\(query)'")
    }
}

Iterating with while let

struct Node {
    value: Int,
    next: Box<Node>?,
}

fn sumLinkedList(head: Box<Node>?): Int {
    var sum = 0
    var current = head

    while let node = current {
        sum += node.value
        current = node.next.clone()
    }

    sum
}

Handling User Input

fn readValidNumber(): Int? {
    // Simulating user input
    Some(42)
}

fn processInput() {
    while let number = readValidNumber() {
        if number == 0 {
            println!("Exiting...")
            break
        }
        println!("Processing: \(number)")
    }
}

When to Use if let vs match

Use if let when:

  • You only care about one specific pattern
  • You want concise code for simple cases
  • The "else" case is trivial or can be ignored

Use match when:

  • You need to handle multiple patterns explicitly
  • You want the compiler to ensure you've handled all cases
  • The logic for different patterns is complex
// Good use of if let - only care about Some case
if let user = findUser(id) {
    greet(user)
}

// Good use of match - need to handle all cases explicitly
match command {
    Command.Start -> startServer(),
    Command.Stop -> stopServer(),
    Command.Restart -> {
        stopServer()
        startServer()
    },
    Command.Status -> printStatus(),
}

The if let and while let constructs provide a more ergonomic way to work with nullable types and pattern matching when you don't need the full power of match. Combined with Oxide's auto-unwrap feature for nullable types, they make working with optional values concise and readable.