Pattern Matching
Pattern matching is how you inspect and decompose values in Ori. It’s the primary way to work with sum types, and it’s more powerful than switch statements in other languages.
The match Expression
The match expression compares a value against patterns:
@color_code (c: Color) -> str = match c {
Red -> "#FF0000",
Green -> "#00FF00",
Blue -> "#0000FF",
};
Structure:
- First argument is the value to match
- Following arguments are pattern-result pairs
->separates pattern from result- First matching pattern wins
Match Returns a Value
match is an expression, so it returns a value:
let description = match status {
Active -> "Running",
Paused -> "On hold",
Stopped -> "Finished",
};
All branches must return the same type.
Pattern Types
Literal Patterns
Match exact values:
@describe_number (n: int) -> str = match n {
0 -> "zero",
1 -> "one",
2 -> "two",
_ -> "many",
};
Works with strings, characters, and booleans too:
@is_yes (s: str) -> bool = match s {
"yes" | "y" | "Y" -> true,
_ -> false,
};
Binding Patterns
Capture the value in a variable:
@double (n: int) -> int = match n {
0 -> 0,
x -> x * 2, // x binds to n's value
};
Wildcard Pattern
_ matches anything and discards it:
@is_zero (n: int) -> bool = match n {
0 -> true,
_ -> false, // Don't care what it is
};
Variant Patterns
Match sum type variants:
type Shape =
| Circle(radius: float)
| Rectangle(width: float, height: float);
@area (s: Shape) -> float = match s {
Circle(radius) -> 3.14159 * radius * radius,
Rectangle(width, height) -> width * height,
};
Struct Patterns
Match on struct fields:
type Point = { x: int, y: int }
@describe_point (p: Point) -> str = match p {
Point { x: 0, y: 0 } -> "origin",
Point { x: 0, y } -> `on y-axis at {y}`,
Point { x, y: 0 } -> `on x-axis at {x}`,
Point { x, y } -> `at ({x}, {y})`,
};
Use .. to ignore remaining fields:
type User = { id: int, name: str, email: str, active: bool }
@user_name (u: User) -> str = match u {
User { name, .. } -> name,
};
Tuple Patterns
Match on tuple elements:
@describe_pair (p: (int, int)) -> str = match p {
(0, 0) -> "origin",
(0, y) -> `y-axis at {y}`,
(x, 0) -> `x-axis at {x}`,
(x, y) -> `({x}, {y})`,
};
List Patterns
Match on list structure:
@describe_list (items: [int]) -> str = match items {
[] -> "empty",
[x] -> `single element: {x}`,
[x, y] -> `two elements: {x} and {y}`,
[first, ..rest] -> `starts with {first}, {len(collection: rest)} more`,
};
List pattern syntax:
[]— empty list[x]— exactly one element[x, y]— exactly two elements[first, ..rest]— first element and remaining list[..init, last]— all but last, and last element
Range Patterns
Match value in a range:
@grade (score: int) -> str = match score {
90..=100 -> "A",
80..90 -> "B",
70..80 -> "C",
60..70 -> "D",
_ -> "F",
};
Advanced Patterns
Or Patterns
Match multiple patterns with |:
@is_primary (c: Color) -> bool = match c {
Red | Green | Blue -> true,
_ -> false,
};
@is_weekend (day: str) -> bool = match day {
"Saturday" | "Sunday" -> true,
_ -> false,
};
At Patterns
Bind a name while also matching a pattern:
@process (s: Status) -> str = match s {
status @ Failed(_) -> {
log_failure(status: status); // Use the full value
"failed"
},
_ -> "ok",
};
Guards
Add conditions with if guards:
@classify (n: int) -> str = match n {
x if x < 0 -> "negative",
0 -> "zero",
x if x < 10 -> "small",
x if x < 100 -> "medium",
_ -> "large",
};
Guards are evaluated after the pattern matches:
@describe_age (age: int) -> str = match age {
a if a < 0 -> "invalid",
a if a < 13 -> "child",
a if a < 20 -> "teenager",
a if a < 65 -> "adult",
_ -> "senior",
};
Combining Patterns
Combine different pattern types:
type Request =
| Get(path: str)
| Post(path: str, body: str)
| Delete(path: str);
@handle (r: Request) -> str = match r {
Get(path) if path.starts_with("/api") -> `API GET: {path}`,
Post(path, body) if body.len() > 1000 -> "Body too large",
Delete("/admin") -> "Cannot delete admin",
Get(path) | Delete(path) -> `Reading: {path}`,
Post(path, _) -> `Writing: {path}`,
};
Exhaustiveness
The compiler ensures you handle all cases.
Complete Coverage
type Direction = North | South | East | West;
// ERROR: non-exhaustive match
@describe (d: Direction) -> str = match d {
North -> "up",
South -> "down",
// Missing East and West!
};
The compiler tells you what’s missing:
error: non-exhaustive match
--> file.ori:5:1
|
| missing patterns: East, West
Catching Everything
Use _ as a catch-all:
@is_north (d: Direction) -> bool = match d {
North -> true,
_ -> false, // Handles South, East, West
};
Unreachable Patterns
The compiler warns about patterns that can never match:
// WARNING: unreachable pattern
@example (n: int) -> int = match n {
_ -> 0, // This matches everything
42 -> 42, // Never reached!
};
Pattern Matching in Functions
Function Clauses
Define functions with pattern-matched parameters:
@factorial (0: int) -> int = 1;
@factorial (n) -> int = n * factorial(n: n - 1);
Guards in Functions
@abs (n: int) -> int if n < 0 = -n;
@abs (n: int) -> int = n;
Combining Clauses and Guards
@classify (0: int) -> str = "zero";
@classify (n) -> str if n < 0 = "negative";
@classify (n) -> str if n < 10 = "small";
@classify (_: int) -> str = "large";
Common Patterns
Handling Option
@display_name (name: Option<str>) -> str = match name {
Some(n) -> n,
None -> "Anonymous",
};
Handling Result
@process_result (r: Result<int, str>) -> str = match r {
Ok(value) -> `Success: {value}`,
Err(error) -> `Error: {error}`,
};
Extracting Nested Data
type Response = {
status: int,
data: Option<{
user: Option<User>,
items: [Item],
}>,
}
@get_user_name (r: Response) -> Option<str> = match r {
Response { data: Some({ user: Some(u), .. }), .. } -> Some(u.name),
_ -> None,
};
Matching Multiple Values
Use tuples to match multiple values at once:
@compare_sizes (a: int, b: int) -> str = match (a, b) {
(0, 0) -> "both zero",
(0, _) -> "first is zero",
(_, 0) -> "second is zero",
(x, y) if x == y -> "equal",
(x, y) if x < y -> "first is smaller",
_ -> "first is larger",
};
Refutability
Patterns can be:
Irrefutable Patterns
Always match — used in let bindings:
let x = 42; // Always matches
let (a, b) = tuple; // Always matches (tuple has two elements)
let Point { x, y } = point; // Always matches
Refutable Patterns
Might not match — used in match:
match option {
Some(x) -> use(x), // Only matches Some
None -> handle_none(), // Only matches None
};
Lists are Refutable
List patterns in let can panic:
let [first, second] = items; // PANIC if items doesn't have exactly 2 elements
Use match for safe list patterns:
let first_two = match items {
[a, b, ..] -> Some((a, b)),
_ -> None,
};
Complete Example
type Json =
| Null
| Bool(value: bool)
| Number(value: float)
| String(value: str)
| Array(items: [Json])
| Object(fields: {str: Json});
@json_type (j: Json) -> str = match j {
Null -> "null",
Bool(_) -> "boolean",
Number(_) -> "number",
String(_) -> "string",
Array(_) -> "array",
Object(_) -> "object",
};
@test_json_type tests @json_type () -> void = {
assert_eq(actual: json_type(j: Null), expected: "null");
assert_eq(actual: json_type(j: Bool(value: true)), expected: "boolean");
assert_eq(actual: json_type(j: Array(items: [])), expected: "array")
}
@json_to_string (j: Json) -> str = match j {
Null -> "null",
Bool(true) -> "true",
Bool(false) -> "false",
Number(n) -> `{n}`,
String(s) -> `"{s}"`,
Array(items) -> {
let parts = for item in items yield json_to_string(j: item);
`[{parts.join(sep: ", ")}]`
},
Object(fields) -> {
let parts = for (key, value) in fields.entries()
yield `"{key}": {json_to_string(j: value)}`;
`\{{parts.join(sep: ", ")}\}`
},
};
@test_json_to_string tests @json_to_string () -> void = {
assert_eq(actual: json_to_string(j: Null), expected: "null");
assert_eq(actual: json_to_string(j: Number(value: 42.0)), expected: "42");
assert_eq(
actual: json_to_string(j: Array(items: [Number(value: 1.0), Number(value: 2.0)]))
expected: "[1, 2]"
)
}
@get_string_field (obj: Json, key: str) -> Option<str> = match obj {
Object(fields) -> match fields[key] {
Some(String(s)) -> Some(s),
_ -> None,
},
_ -> None,
};
@test_get_string_field tests @get_string_field () -> void = {
let obj = Object(fields: {"name": String(value: "Alice")});
assert_eq(actual: get_string_field(obj: obj, key: "name"), expected: Some("Alice"));
assert_eq(actual: get_string_field(obj: obj, key: "age"), expected: None);
assert_eq(actual: get_string_field(obj: Null, key: "name"), expected: None)
}
Quick Reference
Pattern Types
42, "hello", true // Literal
x // Binding
_ // Wildcard
Variant(x) // Sum type variant
{ field, .. } // Struct
(a, b) // Tuple
[] // Empty list
[x, y] // Exact list
[first, ..rest] // List with rest
10..20 // Range
A | B // Or pattern
x @ Pattern // At pattern
x if condition // Guard
Match Expression
match value {
Pattern1 -> result1,
Pattern2 -> result2,
_ -> default,
};
Function Clauses
@fn (0: int) -> int = 0;
@fn (n) -> int if n < 0 = -n;
@fn (n) -> int = n;
What’s Next
Now that you understand pattern matching:
- Option and Result — Handle missing values and errors
- Error Propagation — The
?operator and error traces