Pattern Matching
Pattern matching is one of Keel's most powerful features. It lets you destructure data and branch based on its shape.
Basic Patterns
Literal Patterns
Match exact values:
let x = 1
case x of
0 -> "zero"
1 -> "one"
_ -> "many"
Try itVariable Patterns
Bind values to names:
let point = (3, 4)
case point of
(x, y) -> x + y
Try itWildcard Pattern
Match anything without binding:
let tuple = (42, "ignored")
case tuple of
(x, _) -> x -- Ignore second element
Try itConstructor Patterns
Match custom type variants:
let maybeValue: Maybe Int = Just 42
case maybeValue of
Just x -> "Got value"
Nothing -> "Nothing"
Try itQualified Enum Patterns
Enum patterns support qualified syntax with ::, consistent with how variants are constructed:
type Color = Red | Green | Blue
let color = Color::Green
case color of
Color::Red -> "fire"
Color::Green -> "nature"
Color::Blue -> "sky"
Both qualified (Color::Red) and unqualified (Red) patterns are accepted.
Nested Patterns
Patterns can be nested:
let result: Result (Maybe Int) String = Ok (Just 5)
case result of
Ok (Just x) -> "Success with value: " ++ show x
Ok Nothing -> "Success but empty"
Err msg -> "Error: " ++ msg
List Patterns
Destructure lists using exact matches:
let list = [1, 2]
case list of
[] -> "empty"
[x] -> "single"
[x, y] -> "pair"
[x, y, z] -> "triple"
_ -> "more"
Try itCons Patterns
Use the :: operator to destructure a list into head and tail:
let list = [1, 2, 3]
case list of
x :: xs -> x -- x is the head, xs is the tail
[] -> 0 -- empty list case
Nested cons patterns for multiple elements:
let list = [1, 2, 3, 4]
case list of
a :: b :: rest -> a + b -- match first two elements
x :: xs -> x
[] -> 0
Head and Tail (Recursive)
fn sum : List Int -> Int
fn sum numbers = case numbers of
[] -> 0
x :: xs -> x + sum xs
sum [1, 2, 3, 4, 5] -- 15
Record Patterns
Match record fields:
let person = { name = "Alice", age = 30 }
case person of
{ name, age } -> name
Try itRest Pattern (..)
When matching only some fields, use .. to explicitly ignore the rest. Partial record patterns without .. are an error:
let user = { name = "Bob", age = 25, email = "bob@example.com" }
case user of
{ name, .. } -> "Hello, " ++ name
Try it-- Error: Record pattern is missing fields: age, email
case user of
{ name } -> name -- must use { name, .. } or match all fields
Tuple Patterns
Destructure tuples:
let pair = (3, 4)
case pair of
(0, 0) -> "origin"
(x, 0) -> "on x-axis"
(0, y) -> "on y-axis"
(x, y) -> "somewhere else"
Try itGuards
Add conditions to patterns using if:
let number = 42
case number of
n if n < 0 -> "negative"
n if n == 0 -> "zero"
n if n < 10 -> "small positive"
n if n < 100 -> "medium"
_ -> "large"
Try itAs-Patterns
Bind the whole value while also destructuring:
let maybe: Maybe Int = Just 42
case maybe of
Just x as original -> (x, original)
Nothing -> (0, Nothing)
Try itUseful for accessing both parts and the whole:
let list = [1, 2, 3]
case list of
x :: xs as all -> "Head"
[] -> "empty"
Or-Patterns
Match multiple alternatives:
type Direction = North | South | East | West
let direction = North
case direction of
North | South -> "vertical"
East | West -> "horizontal"
Or-patterns with common variable bindings:
let point = (5, 2)
case point of
(x, 1) | (x, 2) | (x, 3) -> x -- x is bound in all alternatives
_ -> 0
Try itPattern Matching in Let
Destructure in let bindings:
let point = (10, 20)
let (x, y) = point
x + y -- 30
Try itlet user = { name = "Alice", age = 30 }
let { name, age } = user
name
Try itPattern Matching in Function Arguments
fn fst : (a, b) -> a
fn fst pair =
case pair of
(x, _) -> x
fst (1, "hello") -- 1
fn addPairs : (Int, Int) -> (Int, Int) -> (Int, Int)
fn addPairs p1 p2 =
case (p1, p2) of
((x1, y1), (x2, y2)) -> (x1 + x2, y1 + y2)
addPairs (1, 2) (3, 4) -- (4, 6)
Pattern Matching in Lambdas
Lambdas support irrefutable patterns (patterns that always match):
-- Tuple destructuring
let addPair = |(x, y): (Int, Int)| x + y
addPair (3, 4) -- 7
Typed Record Fields in Lambdas
Record fields in lambda patterns can have type annotations. This works standalone — no pipe context needed:
let describe = |{ name: String, age: Int }| name
describe { name = "Alice", age = 30 } -- "Alice"
-- Mixed typed and untyped fields
let f = |{ x: Int, y }| x + y
-- With rest pattern
let g = |{ name: String, .. }| name
The parser disambiguates between types (uppercase) and nested patterns (lowercase):
{ name: String }—Stringis a type annotation{ name: n }—nis a pattern (variable binding)
Note: Lambdas only support irrefutable patterns. Refutable patterns like |Just x| or |[a, b]| are not allowed.
Pattern Completeness Validation
Keel validates pattern matching at compile time to help catch common errors.
Exhaustiveness Checking
The compiler checks that patterns are exhaustive:
type TrafficLight = Red | Yellow | Green
let light = Yellow
case light of
Red -> "stop"
Yellow -> "caution"
Green -> "go"
Guard Exhaustiveness
Always include a catch-all pattern when using guards:
let x = 0
case x of
n if n > 0 -> "positive"
n if n < 0 -> "negative"
_ -> "zero"
Try itUnreachable Pattern Detection
The compiler warns about unreachable patterns (dead code):
case x of
_ -> "anything" -- This matches everything
True -> "true" -- Warning: Unreachable pattern
Pattern Type Validation
Patterns must match the type being matched:
case 42 of
"hello" -> 1 -- Error: Pattern type mismatch: expected Int, but pattern is String
_ -> 0
Branch Return Type Validation
All branches must return compatible types:
case x of
1 -> 42 -- Int
2 -> "hello" -- Error: Case branches have incompatible types
_ -> 0
When types are compatible (e.g., Int and Float), they unify:
let x = 2
case x of
1 -> 42 -- Int
2 -> 3.14 -- Float: compatible, unifies to Float
_ -> 0
Try itBest Practices
- Order patterns specific to general — put catch-all
_last - Handle all cases explicitly when possible
- Use guards for complex conditions
- Avoid deep nesting — refactor into helper functions
- Use as-patterns to avoid reconstructing values
Next Steps
Learn about modules to organize your code into reusable units.