Patterns in Statements: if let, while let, let else and match-expression
Patterns in Zolo are not restricted to match. They appear in four
other syntactic forms, each with a distinct purpose.
if let: matching a single variant
When you only care about one enum variant (typically Some/Just/Ok),
if let is more concise than a two-arm match. The else is optional.
if let with and without else; chaining if let as variant dispatch.
// Feature: `if let` — match a single variant
// Syntax: `if let Pat = expr { ... } else { ... }`
// When to use: you only care about ONE of the enum's variants
// (typically `Some`/`Just`/`Ok`) and want a simple else for the
// rest. More concise than a two-arm `match`.
enum Opt {
Some(int),
None,
}
let val = Opt::Some(42)
if let Opt::Some(x) = val {
print("got: {x}") // got: 42
} else {
print("none")
}
// No else — only runs if matched.
let empty = Opt::None
if let Opt::Some(x) = empty {
print("got: {x}")
}
print("after if let") // after if let
// Chained `if let` for different variants.
enum Shape {
Circle(float),
Square(float),
Empty,
}
fn area(s: Shape) -> float {
if let Shape::Circle(r) = s {
return 3.14159 * r * r
}
if let Shape::Square(side) = s {
return side * side
}
return 0.0
}
print(area(Shape::Circle(5.0))) // 78.53975
print(area(Shape::Square(4.0))) // 16
print(area(Shape::Empty)) // 0
while let: pattern-driven loop
while let repeats while the pattern matches and stops when it fails.
It is the natural idiom for consuming a stack, queue or iterator that signals
end with None/Nothing.
Note:
while lethas partial support in the type checker in some configurations. The example uses the equivalent formloop { match ... }which is safe in all cases.
Stack draining with loop { match ... } — equivalent to while let.
// Feature: `while let` — loop while a pattern matches
// Syntax: `while let Pat = expr { ... }`
// When to use: consume an iterator / pop a stack / drain a queue —
// anything that returns `Some`/`Just` while items remain and
// `None`/`Nothing` once empty.
//
// Note: the binding inside `while let` still has partial type
// checker support. To pass `zolo check`, use the equivalent
// `loop { match ... }` form shown below. The `while let` form is
// preferred once stable.
use std::Array
enum Opt {
Some(int),
None,
}
// Manual stack with pop. Array slicing via `arr[a..b]` is supported
// (returns a new array). `Array.pop` is used here for in-place removal.
struct MyStack {
items: [int],
}
impl MyStack {
fn from(items: [int]) -> MyStack {
return MyStack { items: items }
}
fn take(self) -> Opt {
if self.items.len() == 0 {
return Opt::None
}
let last = self.items.pop()
return Opt::Some(last)
}
}
let s = MyStack::from([1, 2, 3, 4, 5])
// Equivalent to `while let Opt::Some(item) = s.take() { ... }`.
var done = false
while !done {
let p = s.take()
match p {
Opt::Some(item) => print(item),
Opt::None => {
done = true
},
}
}
print("empty")
// expected:
// 5
// 4
// 3
// 2
// 1
// empty
let else: guard clause with binding
let else is the idiom for extracting a value you expect to be there,
with a clean exit otherwise. The else block must diverge
(return, panic, break). After the line, the names from the pattern are
available in the normal scope — with no extra indentation.
let else with enum, with array pattern and chained as clause guards.
// Feature: `let else` — infallible destructuring with diverging fallback
// Syntax: `let Pat = expr else { ... return / break / panic }`
// When to use: extract a value you EXPECT to match, with a clean
// escape hatch when it doesn't. The `else` block must end the
// function (return/panic/etc.). After the line, the pattern's
// names are available in normal scope.
enum Opt {
Some(int),
None,
}
fn double_or_zero(o: Opt) -> int {
let Opt::Some(x) = o else {
return 0
}
// `x` is available from here on.
return x * 2
}
print(double_or_zero(Opt::Some(21))) // 42
print(double_or_zero(Opt::None)) // 0
// let-else with array pattern: take the first or return -1.
fn first(arr: [int]) -> int {
let [head, ..rest] = arr else {
return -1
}
return head
}
print(first([10, 20, 30])) // 10
let empty: [int] = []
print(first(empty)) // -1
// Chaining: several guard clauses in a row.
fn add_first_two(arr: [int]) -> int {
let [a, ..rest] = arr else { return -1 }
let [b, ..rest2] = rest else { return -1 }
return a + b
}
print(add_first_two([3, 4, 5])) // 7
print(add_first_two([3])) // -1
match as an expression
match is an expression: the value of the chosen arm is the value of the entire
match. It can appear on the right-hand side of a let, inside an
arithmetic expression, in string interpolation or as a function body.
match as the RHS of let, inside an arithmetic expression and as a function return.
// Feature: `match` as expression — returns a value
// Syntax: `let v = match expr { pat => val, ... }`
// When to use: a match's value is the value of the matched arm. It
// replaces nested ifs when you only need a value derived from a
// classification.
let n = 7
// Can be the RHS of a let.
let parity = match n % 2 {
0 => "even",
_ => "odd",
}
print(parity) // odd
// Can be part of a larger expression.
let pts = 50 + match n {
n if n > 5 => 100,
n if n > 0 => 50,
_ => 0,
}
print(pts) // 150
// Can appear inside interpolation.
let label = match n {
0 => "zero",
1..=5 => "small",
_ => "big",
}
print("n={n} -> {label}") // n=7 -> big
// `match` as a function return — no `return` needed for a terminal
// match.
fn classify(x: int) -> str {
return match x {
n if n < 0 => "negative",
0 => "zero",
_ => "positive",
}
}
print(classify(-3)) // negative
print(classify(0)) // zero
print(classify(42)) // positive
// expected:
// odd
// 150
// n=7 -> big
// negative
// zero
// positive
Challenge
Rewrite the classify function from the last example using if let instead of
match. Which version do you find more readable for that case?