(or: how the compiler keeps you honest)
The Premise
If there's one feature that makes functional programming feel like a superpower, it's pattern matching. In dynamic Lisp, you'd reach for cond, case, or nested if statements ... and hope you remembered all the cases. In Coalton (like Haskell), the compiler ensures you handle every possibility.
Pattern matching isn't just syntax sugar. It's a way of thinking about data: you define shapes, then you destructure them systematically. The compiler watches your back the whole time.
The Haskell Idea
In Haskell, pattern matching lets you define functions by cases:
describe :: Maybe Int -> String
describe Nothing = "Nothing here"
describe (Just n) = "Got: " ++ show n
The magic is in the exhaustiveness: if Maybe had a third constructor, Haskell would warn you. You can't accidentally forget a case.
Pattern matching also destructures data inline:
head :: [a] -> Maybe a
head [] = Nothing
head (x:_) = Just x
Here, (x:_) binds x to the first element and _ means "I don't care about the rest." The structure of the data is the control flow.
Coalton Translation
Coalton uses match for pattern matching. Let's start simple:
match expression takes a value and a series of patterns. Each pattern can bind variables and must cover every possible case.Create pattern-basics.smt:
#!/usr/bin/env smt run
;; Describe an Optional value
(declare describe-optional ((Optional Integer) -> String))
(define (describe-optional opt)
(match opt
((None) "Nothing here")
((Some n) (<> "Got: " (show n)))))
(define main
(progn
(println (describe-optional None))
(println (describe-optional (Some 42)))))
Run it:
$ ./smt run pattern-basics.smt
Nothing here
Got: 42
Each branch of match handles one constructor. The variable n in (Some n) binds whatever value was wrapped inside.
Nested Patterns
Patterns can nest. Suppose you have a pair of optionals:
#!/usr/bin/env smt run
(declare describe-pair ((Tuple (Optional Integer) (Optional Integer)) -> String))
(define (describe-pair pair)
(match pair
((Tuple (None) (None)) "Both empty")
((Tuple (Some a) (None)) (<> "First: " (show a)))
((Tuple (None) (Some b)) (<> "Second: " (show b)))
((Tuple (Some a) (Some b)) (<> "Both: " (<> (show a) (<> " and " (show b)))))))
(define main
(progn
(println (describe-pair (Tuple None None)))
(println (describe-pair (Tuple (Some 1) None)))
(println (describe-pair (Tuple None (Some 2))))
(println (describe-pair (Tuple (Some 1) (Some 2))))))
Run:
$ ./smt run nested-patterns.smt
Both empty
First: 1
Second: 2
Both: 1 and 2
The nesting mirrors the data structure. You're declaring exactly what shape you expect, and binding variables in one expression.
Type Safety in Action
Here's where the compiler earns its keep. What if we forget a case?
Create incomplete-match.smt:
#!/usr/bin/env smt run
(declare risky-describe ((Optional Integer) -> String))
(define (risky-describe opt)
(match opt
((Some n) (<> "Got: " (show n)))))
;; Oops - forgot None!
(define main
(println (risky-describe (Some 42))))
Run:
$ ./smt run incomplete-match.smt
You'll see a warning like:
COMMON-LISP:WARNING: warn: non-exhaustive match
--> <macroexpansion>:5:4
|
5 | (MATCH OPT
| ______^
| | _____-
6 | || ((SOME N) (<> "Got: " (SHOW N)))))
| ||_______________________________________- missing case (NONE)
| |________________________________________^ non-exhaustive match
The compiler warns you when you haven't handled every possibility.
This is a key benefit: the type system catches missing branches at compile time. No runtime surprises from unhandled None values slipping through—you get immediate feedback.
Wildcards and Catch-Alls
Sometimes you genuinely don't care about certain cases. Use _ as a wildcard:
#!/usr/bin/env smt run
(declare is-some ((Optional Integer) -> Boolean))
(define (is-some opt)
(match opt
((Some _) True)
(_ False)))
;; Helper to convert Boolean to String
(declare show-bool (Boolean -> String))
(define (show-bool b)
(if b "True" "False"))
(define main
(progn
(println (show-bool (is-some (Some 99))))
(println (show-bool (is-some None)))))
Run:
$ ./smt run wildcards.smt
True
False
The first _ inside (Some _) means "there's a value, but I don't need it." The second _ is a catch-all for any remaining cases.
Matching on Lists
Lists have two constructors: Cons and Nil. Pattern matching makes recursive list functions natural:
#!/usr/bin/env smt run
;; Type inference handles list types automatically
(define (my-length lst)
(match lst
((Nil) 0)
((Cons _ rest) (+ 1 (my-length rest)))))
(define (my-sum lst)
(match lst
((Nil) 0)
((Cons x rest) (+ x (my-sum rest)))))
(define main
(progn
(println (<> "Length: " (show (my-length (make-list 1 2 3 4)))))
(println (<> "Sum: " (show (my-sum (make-list 1 2 3 4)))))))
Run:
$ ./smt run list-match.smt
Length: 4
Sum: 10
Each function handles the base case (Nil) and the recursive case (Cons). The patterns are the structure; the logic follows naturally.
Haskell ↔ Coalton Comparison
| Aspect | Haskell | Coalton |
|---|---|---|
| Syntax | case x of { ... } or equation-style |
(match x ...) |
| Exhaustiveness | Compiler warning (with flags) | Compiler warning |
| Wildcards | _ |
_ |
| Nested patterns | Yes | Yes |
| Guards | Yes (| cond = ...) |
No |
if inside the branch body instead:(match opt
((Some n) (if (> n 0) "Positive" "Non-positive"))
((None) "Empty"))Try It Yourself
Exercise 1: Write a safe-div function that returns (Some result) if the divisor is non-zero, or None otherwise.
(declare safe-div (Integer -> Integer -> (Optional Integer)))
Exercise 2: Write a function that takes a list and returns the second element as an Optional:
;; Hint: You'll need to match on nested Cons patterns
(define (second lst)
(match lst
...))
Pattern matching is the compiler asking: "Have you thought about this?"
Every time you write a match, you're declaring the full space of possibilities. The compiler holds you to it. No silent nil. No missing branches. Just exhaustive, explicit, honest code.
Stay tuned for more...