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!
}
2. Group Related Patterns
// 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
- Use
guard letfor early returns - It makes your intent clear and improves readability - Put the most specific patterns first - Ensures they get evaluated before catch-all patterns
- Use guards for additional conditions - When pattern matching alone isn't expressive enough
- Group related patterns with
|- Reduces repetition and groups similar logic - Keep patterns readable - Use helper functions if patterns become too complex
- Prefer exhaustive matching - Use
matchinstead ofif letwhen 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.