Pattern Matching
Pattern matching in March is exhaustive, nested, and deeply integrated with the type system. The compiler verifies that every possible shape of a value is handled.
Basic Match
match expr do
Pattern1 -> result1
Pattern2 -> result2
_ -> default
end
The _ wildcard matches anything and discards the value.
Pattern Catalog
Wildcards and Variables
_ -- match anything, discard
x -- match anything, bind to x
Literal Patterns
match n do
0 -> "zero"
1 -> "one"
_ -> "other"
end
match b do
true -> "yes"
false -> "no"
end
match s do
"hello" -> "greeting"
"bye" -> "farewell"
other -> "unknown: " ++ other
end
Constructor Patterns
type Shape = Circle(Float) | Rect(Float, Float)
match shape do
Circle(r) -> 3.14159 *. r *. r
Rect(w, h) -> w *. h
end
Nullary constructors match with no parens:
match color do
Red -> 0xFF0000
Green -> 0x00FF00
Blue -> 0x0000FF
end
Option and Result
match opt do
None -> "nothing"
Some(v) -> "got: " ++ to_string(v)
end
match result do
Ok(v) -> "success: " ++ to_string(v)
Err(e) -> "error: " ++ e
end
Tuple Patterns
match pair do
(0, _) -> "starts with zero"
(_, 0) -> "ends with zero"
(a, b) -> int_to_string(a + b)
end
List Patterns
match xs do
[] -> "empty"
[x] -> "one: " ++ to_string(x)
[x, y] -> "two elements"
Cons(h, t) -> "head is " ++ to_string(h)
_ -> "longer"
end
[] desugars to Nil. [a, b, c] desugars to Cons(a, Cons(b, Cons(c, Nil))).
Record Patterns
type Point = { x : Float, y : Float }
match p do
{ x = 0.0, y = 0.0 } -> "origin"
{ x = x, y = 0.0 } -> "on x-axis at " ++ float_to_string(x)
{ x = x, y = y } -> "at " ++ float_to_string(x) ++ ", " ++ float_to_string(y)
end
Atom Patterns
match status do
:ok -> "success"
:error -> "failure"
:pending -> "in progress"
end
Qualified Constructor Patterns
When multiple modules define constructors with the same name, qualify them:
match x do
Http.Ok(resp) -> handle_http(resp)
Json.Ok(data) -> handle_json(data)
_ -> ()
end
Negative Integer Patterns
match n do
-1 -> "minus one"
0 -> "zero"
1 -> "one"
_ -> "other"
end
Guards
Guards add a boolean condition to a pattern arm with when:
match n do
x when x < 0 -> "negative"
x when x == 0 -> "zero"
x when x < 100 -> "small positive"
_ -> "large positive"
end
Guards on function heads work the same way:
fn classify(n) when n < 0 do "negative" end
fn classify(n) when n == 0 do "zero" end
fn classify(n) do "positive" end
Exhaustiveness Checking
The compiler verifies that every possible value is matched. If you miss a case, you get a compile-time error:
type Color = Red | Green | Blue
-- compile error: pattern match not exhaustive — missing case: Blue
match color do
Red -> "red"
Green -> "green"
end
Add a wildcard or the missing case to fix it:
match color do
Red -> "red"
Green -> "green"
Blue -> "blue"
end
Exhaustiveness extends to nested patterns. The compiler understands which combinations are possible.
Nested Patterns
Patterns can be nested arbitrarily deep:
type Tree(a) = Leaf | Node(Tree(a), a, Tree(a))
fn depth(t : Tree(a)) : Int do
match t do
Leaf -> 0
Node(Leaf, _, Leaf) -> 1
Node(l, _, r) -> 1 + max(depth(l), depth(r))
end
end
Nested Option:
match (opt_a, opt_b) do
(Some(a), Some(b)) -> a + b
(Some(a), None) -> a
(None, Some(b)) -> b
(None, None) -> 0
end
Multi-Expression Arms
Match arms support multiple expressions — any number of let bindings followed by a final expression:
match result do
Ok(data) ->
let trimmed = String.trim(data)
let upper = String.upcase(trimmed)
println(upper)
true
Err(msg) ->
println("Error: " ++ msg)
false
end
A do ... end wrapper also works for clarity:
match xs do
Cons(h, t) -> do
let doubled = h * 2
Cons(doubled, t)
end
Nil -> Nil
end
Cond (Pattern-Free Multi-Way If)
When you just need multiple boolean conditions, use match without a scrutinee:
match do
score >= 90 -> "A"
score >= 80 -> "B"
score >= 70 -> "C"
score >= 60 -> "D"
_ -> "F"
end
This is equivalent to a chain of if/else but reads more cleanly.
With (Monadic Pattern Matching)
with is for chaining Result/Option bindings without nesting:
with Ok(user) <- authenticate(credentials),
Ok(profile) <- fetch_profile(user.id),
Ok(data) <- load_data(profile.key) do
render(user, profile, data)
else
Err(AuthFailed) -> reply(401, "Unauthorized")
Err(NotFound(kind)) -> reply(404, kind ++ " not found")
Err(Timeout) -> reply(503, "Service unavailable")
end
Each <- binding: if the expression matches the pattern, execution continues with the binding in scope. On mismatch, control passes to the else block (or the non-matching value propagates if there’s no else).
Patterns in Let Bindings
Patterns work directly in let:
let (a, b) = some_pair()
let Some(x) = might_be_some() -- panics if None
let Cons(h, t) = nonempty_list
And in function parameters:
fn fst((a, _)) do a end
fn snd((_, b)) do b end
fn add_points({ x = x1, y = y1 }, { x = x2, y = y2 }) do
{ x = x1 +. x2, y = y1 +. y2 }
end
Multi-Head Functions (Pattern Dispatch)
Consecutive fn declarations with the same name and compatible arities are merged into a single function. The compiler dispatches to the first matching clause:
fn fact(0) do 1 end
fn fact(n) do n * fact(n - 1) end
fn describe(Nil) do "empty list" end
fn describe(Cons(x, _)) do "starts with " ++ to_string(x) end
This is syntactic sugar — the compiler desugars to a single function with an internal match.
Next Steps
- Type System — the types you’re matching against
- Tour — language overview with more examples
- Interfaces — polymorphic dispatch with
interface