Esc
Start typing to search...

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 it

Variable Patterns

Bind values to names:

let point = (3, 4)

case point of
    (x, y) -> x + y
Try it

Wildcard Pattern

Match anything without binding:

let tuple = (42, "ignored")

case tuple of
    (x, _) -> x  -- Ignore second element
Try it

Constructor Patterns

Match custom type variants:

let maybeValue: Maybe Int = Just 42

case maybeValue of
    Just x -> "Got value"
    Nothing -> "Nothing"
Try it

Qualified 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 it

Cons 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 it

Rest 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 it

Guards

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 it

As-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 it

Useful 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 it

Pattern Matching in Let

Destructure in let bindings:

let point = (10, 20)
let (x, y) = point

x + y  -- 30
Try it
let user = { name = "Alice", age = 30 }
let { name, age } = user

name
Try it

Pattern 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 }String is a type annotation
  • { name: n }n is 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 it

Unreachable 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 it

Best Practices

  1. Order patterns specific to general — put catch-all _ last
  2. Handle all cases explicitly when possible
  3. Use guards for complex conditions
  4. Avoid deep nesting — refactor into helper functions
  5. Use as-patterns to avoid reconstructing values

Next Steps

Learn about modules to organize your code into reusable units.