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.