Pattern Matching
Pattern matching is Kit's primary control flow mechanism. It allows you to examine a value's structure and extract data while simultaneously controlling program flow. This guide covers all of Kit's pattern matching features.
Match Expressions
A match expression examines a value against a series of patterns,
executing the code associated with the first pattern that matches:
# Basic match syntax
describe-number = fn(n) =>
match n
| 0 -> "zero"
| 1 -> "one"
| 2 -> "two"
| _ -> "other"
println (describe-number 1) # => one
println (describe-number 5) # => other
Each pattern is introduced with | followed by the pattern and -> before the result expression:
match value
| pattern1 -> expression1
| pattern2 -> expression2
| pattern3 -> expression3
Literal Patterns
Literal patterns match exact values including numbers, strings, and booleans:
# Match numbers
factorial = fn(n) =>
match n
| 0 -> 1
| 1 -> 1
| n -> n * factorial (n - 1)
# Match strings
greet = fn(lang) =>
match lang
| "english" -> "Hello"
| "spanish" -> "Hola"
| "french" -> "Bonjour"
| _ -> "Hi"
# Match booleans
toggle = fn(b) =>
match b
| true -> false
| false -> true
println (greet "spanish") # => Hola
println (toggle true) # => false
Variable Patterns
Variable patterns bind the matched value to a name, making it available in the pattern's expression:
# Bind to a variable
classify = fn(n) =>
match n
| 0 -> "zero"
| x -> "non-zero: ${x}"
println (classify 0) # => zero
println (classify 42) # => non-zero: 42
# Use bound variables in calculations
absolute = fn(n) =>
match n
| 0 -> 0
| x -> if x < 0 then -x else x
Wildcard Pattern
The wildcard pattern _ matches any value but doesn't bind it to a name.
Use it as a catch-all pattern or when you don't need the matched value:
# Catch-all pattern
is-small? = fn(n) =>
match n
| 0 -> true
| 1 -> true
| 2 -> true
| _ -> false
# Ignore values you don't need
always-42 = fn(x) =>
match x
| _ -> 42
println (is-small? 1) # => true
println (is-small? 100) # => false
println (always-42 "anything") # => 42
List Patterns
List patterns allow you to match on list structure and destructure lists into their components:
# Match empty list
is-empty? = fn(list) =>
match list
| [] -> true
| _ -> false
# Destructure head and tail
first = fn(list) =>
match list
| [] -> None
| [x | _] -> Some x
# Recursive list processing
sum = fn(list) =>
match list
| [] -> 0
| [x | rest] -> x + sum rest
# Match multiple elements
starts-with-zero? = fn(list) =>
match list
| [0 | _] -> true
| _ -> false
println (sum [1, 2, 3]) # => 6
println (is-empty? []) # => true
You can nest list patterns to access deeper structure:
# Match nested list structure
second = fn(list) =>
match list
| [] -> None
| [_] -> None
| [_ | [x | _]] -> Some x
println (second [1, 2, 3]) # => Some 2
println (second [1]) # => None
Tuple Patterns
Tuple patterns destructure tuples, extracting their components:
# Destructure pairs
swap = fn(tuple) =>
match tuple
| (a, b) -> (b, a)
# Match specific tuple values
is-origin? = fn(point) =>
match point
| (0, 0) -> true
| _ -> false
# Extract values from tuples
distance-from-origin = fn(point) =>
match point
| (x, y) -> sqrt (x * x + y * y)
# Nested tuple patterns
line-length = fn(line) =>
match line
| ((x1, y1), (x2, y2)) ->
dx = x2 - x1
dy = y2 - y1
sqrt (dx * dx + dy * dy)
println (swap (1, 2)) # => (2, 1)
println (is-origin? (0, 0)) # => true
Constructor Patterns
Constructor patterns match algebraic data type (ADT) variants and extract their associated data:
# Define ADTs
type Option a = Some a | None
type Result a = Ok a | Err String
# Match Option variants
unwrap-or = fn(opt, default) =>
match opt
| Some x -> x
| None -> default
# Match Result variants
handle-result = fn(result) =>
match result
| Ok value -> "Success: ${value}"
| Err msg -> "Error: ${msg}"
# More complex ADT
type Tree a = Leaf a | Branch (Tree a) (Tree a)
tree-sum = fn(tree) =>
match tree
| Leaf x -> x
| Branch left right -> tree-sum left + tree-sum right
my-tree = Branch (Leaf 1) (Branch (Leaf 2) (Leaf 3))
println (tree-sum my-tree) # => 6
Constructor patterns can be combined with other patterns:
# Combine constructor and list patterns
first-ok = fn(results) =>
match results
| [] -> None
| [Ok x | _] -> Some x
| [_ | rest] -> first-ok rest
results = [Err "failed", Ok 42, Ok 100]
println (first-ok results) # => Some 42
Guards
Guards add additional conditions to patterns using if.
A pattern with a guard only matches when both the pattern and the guard
condition are satisfied:
# Basic guard
classify-number = fn(n) =>
match n
| x if x < 0 -> "negative"
| 0 -> "zero"
| x if x > 0 -> "positive"
# Multiple guards
describe-age = fn(age) =>
match age
| n if n < 0 -> "invalid"
| n if n < 13 -> "child"
| n if n < 20 -> "teenager"
| n if n < 65 -> "adult"
| _ -> "senior"
println (classify-number -5) # => negative
println (describe-age 15) # => teenager
Guards work with any pattern type:
# Guards with list patterns
first-positive = fn(list) =>
match list
| [] -> None
| [x | _] if x > 0 -> Some x
| [_ | rest] -> first-positive rest
# Guards with constructor patterns
filter-small-ok = fn(result) =>
match result
| Ok x if x < 10 -> Ok x
| Ok x -> Err "too large"
| err -> err
println (first-positive [-1, -2, 3, 4]) # => Some 3
Exhaustiveness
Kit ensures that match expressions are exhaustive, meaning they handle all possible cases. This prevents runtime errors from unhandled values:
# Non-exhaustive match (compile error!)
bad-example = fn(opt) =>
match opt
| Some x -> x
# Missing: | None -> ...
# Exhaustive match (correct)
good-example = fn(opt) =>
match opt
| Some x -> x
| None -> 0
Use wildcard patterns to ensure exhaustiveness:
# Handle all remaining cases with wildcard
safe-classify = fn(value) =>
match value
| 0 -> "zero"
| 1 -> "one"
| _ -> "other" # Handles all other numbers
Match Macros
Kit provides three concise match macros that simplify common pattern matching scenarios:
is, as, and guard. These keywords expand into full
match expressions at compile time.
The is Macro
The is macro checks if a value matches a pattern, returning a boolean. It's
perfect for conditionals where you just need to test a pattern.
# Syntax: expr is Pattern
# Returns: true if expr matches Pattern, false otherwise
# Check ADT variant
is-passed? = status is Passed
# Use in conditionals
if result.status is Passed then "ok" else "fail"
# Check with nested patterns
has-name? = user is Some {name: "Alice", ..}
# Check non-empty list
has-items? = list is [_ | _]
# Combine with boolean operators
both-ok? = result1 is Ok _ and result2 is Ok _
The is macro expands to a match expression:
# This:
status is Passed
# Expands to:
match status
| Passed -> true
| _ -> false
The as Macro
The as macro extracts a value when a pattern matches, with a default value
otherwise. It's useful for accessing data inside ADTs without verbose match blocks.
# Syntax: expr as Pattern -> consequent else alternative
# Extract from Option with default
name = get-user(id) as Some {name, ..} -> name else "Anonymous"
# Extract from Result
stdout = run-result as Ok {stdout, ..} -> stdout else ""
# Transform the matched value
doubled = opt as Some n -> n * 2 else 0
# Access list head with default
first = items as [x | _] -> x else 0
The as macro expands to a match expression:
# This:
get-user(id) as Some u -> u.name else "Anonymous"
# Expands to:
match get-user(id)
| Some u -> u.name
| _ -> "Anonymous"
The guard Macro
The guard macro binds a pattern or returns early from a function. It's
similar to Swift's guard statement and complements Kit's ?! operator,
but allows custom return values and works with any pattern.
# Syntax: guard Pattern = expr else return-expr
# Binds pattern variables if match succeeds, returns early otherwise
process-user = fn(id) =>
# Bind or return early
guard Some user = get-user(id) else Err "User not found"
guard Ok validated = validate(user) else Err "Validation failed"
# Continue with bound variables
Ok (transform validated)
# Compared to nested matches:
process-user-verbose = fn(id) =>
match get-user(id)
| Some user ->
match validate(user)
| Ok validated -> Ok (transform validated)
| Err e -> Err "Validation failed"
| None -> Err "User not found"
Note: The guard macro is particularly useful for
reducing nesting in functions that need multiple pattern checks before proceeding.
Compare it with the ?! operator which propagates errors unchanged:
# ?! propagates the original error
process = fn(path) =>
content = File.read(path) ?! # Returns original Err if read fails
Ok content
# guard allows custom error values
process = fn(path) =>
guard Ok content = File.read(path) else Err "Could not read ${path}"
Ok content
Practical Examples
Pattern matching shines in real-world scenarios. Here are some practical examples:
JSON-like Data Processing
type JsonValue =
| JNull
| JBool Bool
| JNumber Int
| JString String
| JArray [JsonValue]
| JObject [(String, JsonValue)]
stringify = fn(json) =>
match json
| JNull -> "null"
| JBool true -> "true"
| JBool false -> "false"
| JNumber n -> to-string n
| JString s -> "\"${s}\""
| JArray items ->
strs = map stringify items
"[${join \", \" strs}]"
| JObject pairs ->
format-pair = fn((k, v)) => "\"${k}\": ${stringify v}"
strs = map format-pair pairs
"{${join \", \" strs}}"
State Machine
type State = Idle | Running | Paused | Stopped
type Event = Start | Pause | Resume | Stop
transition = fn(state, event) =>
match (state, event)
| (Idle, Start) -> Running
| (Running, Pause) -> Paused
| (Paused, Resume) -> Running
| (Running, Stop) -> Stopped
| (Paused, Stop) -> Stopped
| (s, _) -> s # Invalid transitions keep current state
current = Idle
current = transition current Start # Running
current = transition current Pause # Paused
current = transition current Resume # Running
List Operations
# Take first n elements
take = fn(n, list) =>
match (n, list)
| (0, _) -> []
| (_, []) -> []
| (n, [x | rest]) -> cons x (take (n - 1) rest)
# Drop first n elements
drop = fn(n, list) =>
match (n, list)
| (0, xs) -> xs
| (_, []) -> []
| (n, [_ | rest]) -> drop (n - 1) rest
# Zip two lists together
zip = fn(xs, ys) =>
match (xs, ys)
| ([], _) -> []
| (_, []) -> []
| ([x | xs-rest], [y | ys-rest]) ->
cons (x, y) (zip xs-rest ys-rest)
println (take 3 [1, 2, 3, 4, 5]) # => [1, 2, 3]
println (zip [1, 2] ["a", "b"]) # => [(1, "a"), (2, "b")]
Best Practices
- Always be exhaustive: Ensure your match expressions handle all possible cases to prevent runtime errors.
- Order patterns carefully: Patterns are checked top-to-bottom; place more specific patterns before general ones.
- Use guards for complex conditions: When a simple pattern isn't enough, guards keep your code clear.
- Prefer pattern matching over if-else: Pattern matching is more expressive and safer for complex conditionals.
- Name bound variables clearly: Use descriptive names for variables bound in patterns.
- Use wildcards when appropriate: If you don't need a value, use
_to signal intent.
Next Steps
Now that you understand pattern matching, explore related topics:
- Language Tour - Review Kit's core features
- List Functions - Learn about list processing functions
- Standard Library - Explore all built-in functions