Coalton via Haskell: Pattern Matching

Coalton via Haskell: Pattern Matching

(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:

💡
The 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.

💡
Use wildcards sparingly. They're convenient, but explicit patterns document intent better. A catch-all at the end can also hide new cases you should handle if the type evolves.

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
💡
Guards: Haskell allows guard conditions attached directly to pattern branches. Coalton doesn't have this syntax: you'd add an 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...