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!")
}
}
Processing Results from a Search
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.