Match Expressions
Master match expressions for discriminated unions, tuples, records, and lists with exhaustiveness checking.
Match Expressions in F#
Master pattern matching in F# with free flashcards and spaced repetition practice. This lesson covers match syntax, pattern types, guards, and when expressions—essential concepts for writing expressive functional code in F#.
💻 Welcome to Pattern Matching
Pattern matching is one of F#'s most powerful features, allowing you to decompose data structures and make decisions based on their shape and content. Think of it as a supercharged switch statement that can destructure values, test conditions, and bind variables—all in one elegant syntax.
Match expressions evaluate an input value against a series of patterns and execute the code associated with the first matching pattern. Unlike imperative if-else chains, pattern matching is declarative, exhaustive (the compiler warns about missing cases), and incredibly expressive.
🔍 Why Pattern Matching Matters:
- Type Safety: The compiler ensures you handle all cases
- Readability: Complex conditional logic becomes clear and concise
- Destructuring: Extract values from tuples, records, lists, and discriminated unions in one step
- Exhaustiveness Checking: Never forget an edge case again
💡 Did you know? Pattern matching traces its roots to ML (Meta Language) from the 1970s. F# inherits this powerful feature from its OCaml ancestry, making it a cornerstone of functional programming on .NET!
🎯 Core Concepts
Basic Match Syntax
The fundamental structure of a match expression follows this pattern:
match expression with
| pattern1 -> result1
| pattern2 -> result2
| pattern3 -> result3
F# evaluates the expression, then tests it against each pattern from top to bottom. The first matching pattern wins, and its corresponding result is returned. All branches must return the same type.
Example:
let describeNumber n =
match n with
| 0 -> "zero"
| 1 -> "one"
| 2 -> "two"
| _ -> "many" // underscore is the wildcard pattern
let result = describeNumber 5 // "many"
Pattern Types
F# supports multiple pattern types, each serving different purposes:
| Pattern Type | Syntax | Use Case |
|---|---|---|
| Constant | | 42 -> |
Match specific values |
| Wildcard | | _ -> |
Match anything (catch-all) |
| Variable | | x -> |
Bind matched value to name |
| Tuple | | (x, y) -> |
Destructure tuples |
| List | | head::tail -> |
Decompose lists |
| Record | | { Name = n } -> |
Extract record fields |
| Union Case | | Some x -> |
Discriminated union matching |
| Type Test | | :? string -> |
Runtime type checking |
Wildcard Pattern (_)
The wildcard pattern _ matches anything but doesn't bind the value to a name. It's perfect for default cases:
let classify score =
match score with
| 100 -> "Perfect!"
| s when s >= 90 -> "A"
| s when s >= 80 -> "B"
| _ -> "Below B" // everything else
⚠️ Important: Place the wildcard last! Since patterns are evaluated top-to-bottom, a wildcard will match everything and prevent later patterns from ever executing.
Variable Binding Pattern
When you use an identifier (not underscore), the matched value is bound to that variable:
let processValue input =
match input with
| 0 -> "Got zero"
| n -> sprintf "Got %d" n // 'n' binds to the value
Here, n captures the matched value, making it available in the result expression.
Tuple Patterns
Tuple destructuring lets you extract multiple values simultaneously:
let evaluatePoint point =
match point with
| (0, 0) -> "Origin"
| (x, 0) -> sprintf "On X-axis at %d" x
| (0, y) -> sprintf "On Y-axis at %d" y
| (x, y) -> sprintf "Point at (%d, %d)" x y
let result = evaluatePoint (3, 4) // "Point at (3, 4)"
💡 Tip: You can nest patterns! | (0, (x, y)) matches a nested tuple with first element zero.
List Patterns
List patterns use the cons operator :: to separate head from tail:
let rec sumList lst =
match lst with
| [] -> 0 // empty list
| [x] -> x // single element
| head::tail -> head + sumList tail // head + recursive sum of tail
let total = sumList [1; 2; 3; 4] // 10
🧠 Mnemonic: Think of :: as "cons-truction" - you're deconstructing what was constructed with cons.
You can match multiple elements:
match myList with
| [] -> "empty"
| [x] -> "one element"
| [x; y] -> "two elements"
| x::y::rest -> "two or more elements"
When Guards
Guard clauses add conditional tests to patterns using the when keyword:
let categorizeAge age =
match age with
| a when a < 0 -> "Invalid"
| a when a < 13 -> "Child"
| a when a < 20 -> "Teen"
| a when a < 65 -> "Adult"
| _ -> "Senior"
let category = categorizeAge 25 // "Adult"
Guards are evaluated after the pattern matches, adding extra filtering logic.
⚠️ Watch out: Guards can make patterns non-exhaustive! The compiler can't always verify you've covered all cases when guards are involved.
Option Type Matching
The Option type (Some or None) is a common match expression target:
let tryDivide x y =
if y = 0 then None
else Some (x / y)
let displayResult opt =
match opt with
| Some value -> sprintf "Result: %d" value
| None -> "Cannot divide by zero"
let output = displayResult (tryDivide 10 2) // "Result: 5"
This eliminates null reference exceptions and makes error handling explicit!
Record Patterns
Extract record fields directly in the pattern:
type Person = { Name: string; Age: int }
let greet person =
match person with
| { Name = "Alice" } -> "Hi Alice!"
| { Age = age } when age < 18 -> "Hello young person"
| { Name = name; Age = age } -> sprintf "%s is %d years old" name age
let message = greet { Name = "Bob"; Age = 25 }
You don't need to match all fields—partial matching is allowed!
Discriminated Unions
Discriminated unions (DUs) are the perfect match for match expressions:
type Shape =
| Circle of radius: float
| Rectangle of width: float * height: float
| Triangle of base_: float * height: float
let calculateArea shape =
match shape with
| Circle radius -> System.Math.PI * radius * radius
| Rectangle (width, height) -> width * height
| Triangle (base_, height) -> 0.5 * base_ * height
let area = calculateArea (Circle 5.0) // 78.54...
The compiler enforces exhaustiveness—forget a case, and you'll get a warning!
📖 Detailed Examples
Example 1: Recursive List Processing
🔧 Try this: Implement a function that finds the maximum value in a list using pattern matching.
let rec findMax lst =
match lst with
| [] -> failwith "Empty list has no maximum"
| [x] -> x
| head::tail ->
let maxTail = findMax tail
if head > maxTail then head else maxTail
// Usage
let numbers = [3; 7; 2; 9; 4; 1]
let maximum = findMax numbers // 9
Explanation:
- Base case 1: Empty list throws an error (can't have a max)
- Base case 2: Single element is the max
- Recursive case: Compare head with the max of the tail
This demonstrates how pattern matching makes recursive algorithms elegant and clear.
Example 2: Expression Evaluator
Let's build a simple expression evaluator for arithmetic:
type Expr =
| Number of int
| Add of Expr * Expr
| Multiply of Expr * Expr
| Subtract of Expr * Expr
let rec evaluate expr =
match expr with
| Number n -> n
| Add (left, right) -> evaluate left + evaluate right
| Multiply (left, right) -> evaluate left * evaluate right
| Subtract (left, right) -> evaluate left - evaluate right
// Build expression: (5 + 3) * 2
let myExpr = Multiply(Add(Number 5, Number 3), Number 2)
let result = evaluate myExpr // 16
Explanation:
- Each discriminated union case represents an operation
- Pattern matching destructures the expression tree
- Recursive calls evaluate sub-expressions
- The entire evaluator is just 7 lines of clear, type-safe code!
🌍 Real-world analogy: This is how compilers parse and evaluate code—they build abstract syntax trees (ASTs) and traverse them with pattern matching.
Example 3: State Machine with Guards
Model a traffic light controller with state transitions:
type Light = Red | Yellow | Green
type Action = TimerExpired | EmergencyVehicle
let transition currentLight action =
match currentLight, action with
| Red, TimerExpired -> Green
| Green, TimerExpired -> Yellow
| Yellow, TimerExpired -> Red
| _, EmergencyVehicle -> Green // always go green for emergency
| light, _ -> light // no change for other combinations
// Simulate transitions
let light1 = transition Red TimerExpired // Green
let light2 = transition Green EmergencyVehicle // Green
let light3 = transition Yellow TimerExpired // Red
Explanation:
- We match on a tuple
(currentLight, action) - Guards aren't needed here—pattern order handles priority
- The wildcard
_creates default behavior - Emergency vehicles always trigger green, regardless of current state
Example 4: Active Patterns (Advanced)
Active patterns let you create custom pattern recognizers:
// Define an active pattern to categorize numbers
let (|Even|Odd|) n =
if n % 2 = 0 then Even else Odd
let describeNumber n =
match n with
| Even -> sprintf "%d is even" n
| Odd -> sprintf "%d is odd" n
let result1 = describeNumber 4 // "4 is even"
let result2 = describeNumber 7 // "7 is odd"
Explanation:
(|Even|Odd|)defines a complete active pattern (all inputs covered)- The function returns a discriminated union case (Even or Odd)
- Match expressions can now use these custom patterns
- This abstracts complex recognition logic into reusable patterns
💡 Did you know? Active patterns are unique to F#—they don't exist in most other functional languages!
⚠️ Common Mistakes
1. Unreachable Patterns (Order Matters!)
❌ Wrong:
match x with
| _ -> "anything"
| 0 -> "zero" // This will NEVER execute!
✅ Correct:
match x with
| 0 -> "zero"
| _ -> "anything" // Wildcard must come last
Why: Patterns are evaluated top-to-bottom. Once a pattern matches, evaluation stops.
2. Non-Exhaustive Matches
❌ Wrong:
type Color = Red | Green | Blue
let describe color =
match color with
| Red -> "red"
| Green -> "green"
// Missing Blue! Compiler warning!
✅ Correct:
let describe color =
match color with
| Red -> "red"
| Green -> "green"
| Blue -> "blue" // All cases covered
Why: The compiler detects missing patterns. Always handle all cases or use a wildcard default.
3. Forgetting Parentheses in Tuple Patterns
❌ Wrong:
match point with
| x, y -> x + y // Syntax error!
✅ Correct:
match point with
| (x, y) -> x + y // Tuples need parentheses
4. Misunderstanding Guard Scope
❌ Wrong:
match x with
| n when n > 0 -> "positive"
| n when n < 0 -> "negative"
// Missing the n = 0 case when guards are involved!
✅ Correct:
match x with
| n when n > 0 -> "positive"
| n when n < 0 -> "negative"
| _ -> "zero" // Explicit default needed
Why: Guards make patterns conditional, potentially leaving gaps.
5. Shadowing Variables Accidentally
❌ Confusing:
let x = 10
match input with
| x -> printfn "%d" x // 'x' shadows outer 'x', always matches!
✅ Clear:
let x = 10
match input with
| 10 -> "matches ten"
| y -> printfn "Got %d, outer x is %d" y x // Use different name
6. Incorrect List Pattern Syntax
❌ Wrong:
match lst with
| head:tail -> ... // Single colon is wrong!
✅ Correct:
match lst with
| head::tail -> ... // Double colon (cons operator)
🎯 Key Takeaways
- Match expressions evaluate patterns top-to-bottom, executing the first match
- Always put wildcard patterns (
_) last to avoid unreachable code - The compiler checks exhaustiveness—cover all cases or use a default
- Guards (
when) add conditional logic but may create non-exhaustive matches - Destructuring extracts values from tuples, lists, records, and DUs inline
- List patterns use double colon
::to separate head from tail - Option types (
Some/None) eliminate null reference exceptions - Pattern matching is declarative and type-safe, unlike imperative
if-else - Active patterns let you create custom, reusable pattern recognizers
- Discriminated unions and pattern matching form the backbone of F# program design
🧠 Memory Device - "MATCH":
- Multiple patterns tested in order
- All cases must be covered (exhaustiveness)
- Types must align across all branches
- Construct bindings while destructuring
- Hierarchy matters—specific before general
📚 Further Study
- Microsoft F# Language Reference - Pattern Matching: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/pattern-matching
- F# for Fun and Profit - Match Expressions: https://fsharpforfunandprofit.com/posts/match-expression/
- Active Patterns Deep Dive: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/active-patterns
📋 Quick Reference Card
| Pattern | Syntax | Example |
|---|---|---|
| Constant | | 42 -> | | 0 -> "zero" |
| Wildcard | | _ -> | | _ -> "default" |
| Variable | | x -> | | n -> n + 1 |
| Tuple | | (x, y) -> | | (0, y) -> y |
| List Empty | | [] -> | | [] -> 0 |
| List Cons | | h::t -> | | h::t -> h |
| Option Some | | Some x -> | | Some v -> v |
| Option None | | None -> | | None -> 0 |
| Record | | {Field=x} -> | | {Name=n} -> n |
| Guard | | x when x>0 -> | | n when n%2=0 -> "even" |
| OR Pattern | | 1 | 2 -> | | "yes" | "y" -> true |
🔑 Remember: Patterns match top-to-bottom, put wildcard last, compiler checks exhaustiveness!