Skip to content

Chapter 5: Pattern Matching Power

Pattern matching is Tova's crown jewel. It's like switch statements from other languages, but vastly more powerful — it can match values, destructure data, check conditions, and the compiler ensures you handle every case.

This chapter takes you from understanding patterns to wielding them with confidence. By the end, you'll build a command parser that showcases every pattern type.

The Match Expression

Every match has a subject (the value being matched) and arms (the patterns to try):

tova
match subject {
  pattern1 => result1
  pattern2 => result2
  _ => default_result
}

Arms are tried top to bottom. The first match wins. _ is the wildcard that matches anything.

Crucially, match is an expression — it returns a value:

tova
label = match status {
  "active" => "ON"
  "paused" => "PAUSED"
  _ => "OFF"
}

The Complete Pattern Catalog

1. Literal Patterns

Match exact values — numbers, strings, booleans:

tova
match status_code {
  200 => "OK"
  404 => "Not Found"
  500 => "Server Error"
  _ => "Unknown"
}

match direction {
  "north" => go_up()
  "south" => go_down()
  _ => stay()
}

2. Range Patterns

Match a range of values. .. is exclusive, ..= is inclusive:

tova
fn grade(score) {
  match score {
    90..=100 => "A"
    80..90 => "B"     // 80 to 89
    70..80 => "C"
    60..70 => "D"
    0..60 => "F"
    _ => "Invalid"
  }
}

3. Binding Patterns

Capture the matched value into a variable:

tova
match value {
  0 => "zero"
  n => "got {n}"
}

4. Guard Patterns

Add if conditions after any pattern:

tova
match temperature {
  t if t < 0 => "freezing"
  t if t < 15 => "cold"
  t if t < 25 => "comfortable"
  t => "hot ({t}°)"
}

Guards are checked after the pattern matches. If the guard fails, matching continues to the next arm.

5. Variant Patterns

Match type variants and destructure their fields:

tova
type Shape {
  Circle(radius: Float)
  Rectangle(width: Float, height: Float)
}

fn area(shape) {
  match shape {
    Circle(r) => 3.14159 * r * r
    Rectangle(w, h) => w * h
  }
}

6. Array Patterns

Match arrays by structure:

tova
fn describe(items) {
  match items {
    [] => "empty"
    [x] => "just {x}"
    [x, y] => "{x} and {y}"
    [x, ...rest] => "{x} and {len(rest)} more"
  }
}

7. String Concat Patterns

Match and split strings with ++:

tova
fn classify_url(url) {
  match url {
    "https://" ++ domain => "Secure: {domain}"
    "http://" ++ domain => "Insecure: {domain}"
    "/api/" ++ path => "API endpoint: {path}"
    _ => "Other: {url}"
  }
}

8. Wildcard Pattern

_ matches anything without binding:

tova
match result {
  Ok(value) => use(value)
  Err(_) => handle_error()    // don't care what the error is
}

9. Object Patterns

Match and destructure objects by their properties:

tova
fn describe_point(point) {
  match point {
    { x: 0, y: 0 } => "origin"
    { x: 0, y } => "on y-axis at {y}"
    { x, y: 0 } => "on x-axis at {x}"
    { x, y } => "({x}, {y})"
  }
}

print(describe_point({ x: 0, y: 0 }))    // "origin"
print(describe_point({ x: 3, y: 0 }))    // "on x-axis at 3"
print(describe_point({ x: 5, y: 7 }))    // "(5, 7)"

Object patterns match when the value has the specified keys. Unmentioned keys are ignored — { x } matches any object with an x property, regardless of other properties.

Try "All Pattern Types" in Playground

10. Tuple Patterns

Match and destructure tuples by position:

tova
fn describe_pair(pair) {
  match pair {
    (0, 0) => "origin"
    (x, 0) => "on x-axis at {x}"
    (0, y) => "on y-axis at {y}"
    (x, y) => "point ({x}, {y})"
  }
}

print(describe_pair((0, 0)))     // "origin"
print(describe_pair((3, 0)))     // "on x-axis at 3"
print(describe_pair((5, 7)))     // "point (5, 7)"

Tuple patterns are useful with functions that return multiple values:

tova
fn classify_divmod(a, b) {
  match divmod(a, b) {
    (_, 0) => "evenly divisible"
    (q, r) => "{a} = {b} * {q} + {r}"
  }
}

11. Boolean and Nil Patterns

Match booleans and nil directly:

tova
fn truthiness(val) {
  match val {
    true => "yes"
    false => "no"
    nil => "nothing"
    _ => "something else"
  }
}

Combining Patterns

The real power of pattern matching comes from combining these pattern types.

Guards on Variant Patterns

tova
type Account {
  Free(email: String)
  Premium(email: String, months_left: Int)
}

fn access_level(account) {
  match account {
    Premium(_, months) if months > 0 => "full"
    Premium(_, _) => "expired"
    Free(_) => "basic"
  }
}

Array Patterns with Guards

tova
fn process_args(args) {
  match args {
    [] => show_help()
    [cmd] if cmd == "--help" => show_help()
    [cmd] if cmd == "--version" => show_version()
    [cmd, ...rest] => run_command(cmd, rest)
  }
}

Nested Destructuring

Patterns can nest arbitrarily deep — variant inside variant, array inside object, any combination:

tova
type Tree {
  Leaf(value: Int)
  Node(left: Tree, right: Tree)
}

fn tree_sum(tree) {
  match tree {
    Leaf(v) => v
    Node(Leaf(l), Leaf(r)) => l + r
    Node(left, right) => tree_sum(left) + tree_sum(right)
  }
}

Nested Object Patterns

Objects inside objects can be destructured in a single match:

tova
fn get_city(user) {
  match user {
    { address: { city: "Portland" } } => "Local user"
    { address: { city, country: "US" } } => "US user in {city}"
    { address: { city, country } } => "International: {city}, {country}"
    _ => "No address"
  }
}

fn api_response(response) {
  match response {
    { status: 200, data: { users: [first, ...rest] } } => {
      print("First user: {first.name}, {len(rest)} more")
    }
    { status: 200, data: { users: [] } } => {
      print("No users found")
    }
    { status, error: { message } } => {
      print("Error {status}: {message}")
    }
  }
}

Exhaustive Checking

The compiler warns you when a match doesn't cover all cases:

tova
type Direction { North, South, East, West }

fn to_arrow(dir) {
  match dir {
    North => "↑"
    South => "↓"
    // Warning: non-exhaustive match — missing: East, West
  }
}

This is especially valuable with Result and Option:

tova
fn process(result) {
  match result {
    Ok(v) => print(v)
    // Warning: non-exhaustive — missing: Err
  }
}

Always Handle Every Case

Even if you think a case can't happen, match it with _ and log an error. The compiler's exhaustive checking is one of Tova's biggest safety features — work with it, not against it.

Match in Practice: Expression Trees

Here's a powerful real-world use — an expression evaluator using recursive match:

tova
type Expr {
  Num(value: Float)
  Add(left: Expr, right: Expr)
  Mul(left: Expr, right: Expr)
  Neg(expr: Expr)
}

fn eval_expr(expr) {
  match expr {
    Num(v) => v
    Add(l, r) => eval_expr(l) + eval_expr(r)
    Mul(l, r) => eval_expr(l) * eval_expr(r)
    Neg(e) => 0.0 - eval_expr(e)
  }
}

fn show(expr) {
  match expr {
    Num(v) => toString(v)
    Add(l, r) => "({show(l)} + {show(r)})"
    Mul(l, r) => "({show(l)} * {show(r)})"
    Neg(e) => "-{show(e)}"
  }
}

// Build: (2 + 3) * -4
expr = Mul(Add(Num(2.0), Num(3.0)), Neg(Num(4.0)))
print("{show(expr)} = {eval_expr(expr)}")
// "((2 + 3) * -4) = -20"

Notice how eval_expr and show are both recursive — each variant of Expr is handled by exactly one arm, and the compiler ensures none are missed.

Try "Expression Tree" in Playground

Patterns in Destructuring

The same patterns you use in match also work for direct destructuring. No keyword is needed -- just write the pattern on the left side of the assignment:

tova
// Array destructuring with rest
[first, second, ...remaining] = [1, 2, 3, 4, 5]
print(first)        // 1
print(remaining)    // [3, 4, 5]

// Object destructuring
{ name, age } = { name: "Alice", age: 30, role: "admin" }
print(name)         // "Alice"

// Nested destructuring
{ address: { city, country } } = user
print("Lives in {city}, {country}")

// Tuple destructuring
(x, y) = get_coordinates()

Destructuring is a convenience for when you know the shape of your data. If the data might not match the pattern, use match instead so you can handle both cases.

Match vs. If Chains

When should you use match instead of if/elif?

Use match when:

  • Comparing one value against multiple possibilities
  • Destructuring data (variants, arrays, strings)
  • You want exhaustive checking
  • The logic maps a value to a result

Use if/elif when:

  • Conditions involve multiple unrelated variables
  • You need complex boolean logic (&&, ||)
  • Only two or three branches
tova
// Match is clearer here
label = match status {
  "active" => "Running"
  "paused" => "On Hold"
  "completed" => "Done"
  _ => "Unknown"
}

// If/elif is clearer here
if age >= 18 && has_id {
  grant_access()
} elif is_accompanied {
  grant_limited_access()
} else {
  deny_access()
}

Multi-Statement Arms

When a match arm needs multiple statements, use braces:

tova
match command {
  "save" => {
    data = collect_data()
    write_file(data)
    print("Saved successfully")
  }
  "load" => {
    data = read_file()
    update_state(data)
    print("Loaded {len(data)} records")
  }
  _ => print("Unknown command")
}

The last expression in the block is still the return value if you're using match as an expression.

Project: Command Parser

Let's build a fully-featured command parser that combines variant types with string pattern matching:

tova
type Command {
  Help
  Quit
  Echo(message: String)
  Add(a: Float, b: Float)
  Repeat(times: Int, message: String)
  Unknown(input: String)
}

fn parse_command(input) {
  cleaned = trim(input)
  match cleaned {
    "help" => Help
    "quit" => Quit
    "echo " ++ msg => Echo(msg)
    "add " ++ args => {
      parts = split(args, " ")
      if len(parts) == 2 {
        Add(toFloat(parts[0]), toFloat(parts[1]))
      } else {
        Unknown(cleaned)
      }
    }
    "repeat " ++ args => {
      parts = split(args, " ")
      if len(parts) >= 2 {
        n = toInt(parts[0])
        msg = join(drop(parts, 1), " ")
        Repeat(n, msg)
      } else {
        Unknown(cleaned)
      }
    }
    other => Unknown(other)
  }
}

fn execute(cmd) {
  match cmd {
    Help => print("Commands: help, quit, echo <msg>, add <a> <b>, repeat <n> <msg>")
    Quit => print("Goodbye!")
    Echo(msg) => print(msg)
    Add(a, b) => print("{a} + {b} = {a + b}")
    Repeat(n, msg) => {
      for _ in range(n) {
        print(msg)
      }
    }
    Unknown(input) => print("Unknown: {input}")
  }
}

// Run some commands
commands = ["help", "echo Hello World!", "add 3.14 2.86", "repeat 3 Tova!", "nonsense"]
for input in commands {
  print("> {input}")
  execute(parse_command(input))
  print("")
}

The key technique: parse into a variant type, then match on the variant to execute. This separates parsing from execution, making both easier to test and extend.

Try "Command Parser" in Playground

Exercises

Exercise 5.1: Extend the expression evaluator to support Div(left, right) and Pow(base, exponent). Handle division by zero by returning 0.0 with a printed warning.

Exercise 5.2: Write a simplify(expr) function that performs basic algebraic simplification:

  • Add(Num(0), x) or Add(x, Num(0))x
  • Mul(Num(1), x) or Mul(x, Num(1))x
  • Mul(Num(0), _) or Mul(_, Num(0))Num(0)
  • Neg(Neg(x))x

Exercise 5.3: Write a parse_path(path) function using string concat patterns that returns a structured object. For example, parse_path("/users/alice/profile.json") returns { segments: ["users", "alice", "profile.json"], filename: "profile.json", extension: "json" }.

Challenge

Build a calculator language parser. Support:

  1. Parsing expressions like "3 + 4 * 2" into your Expr type
  2. Respecting operator precedence (multiplication before addition)
  3. Supporting parentheses for grouping
  4. A format(expr) function that pretty-prints the tree
  5. An eval_expr(expr) function that computes the result

← Previous: String Craft | Next: Designing with Types →

Released under the MIT License.