Skip to content

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.

13-if-let.zolo
Playground
// 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 let has partial support in the type checker in some configurations. The example uses the equivalent form loop { match ... } which is safe in all cases.

Stack draining with loop { match ... } — equivalent to while let.

14-while-let.zolo
Playground
// 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.

15-let-else.zolo
Playground
// 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.

16-match-as-expression.zolo
Playground
// 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?

enespt-br