Dive C3.03.1
Familiar ground — match you already know
The first thing you reach for in Elixir is the first thing you reach for in F#.
An Elixir developer matches all day: case over a tagged tuple, a clause per
function head, when guards, a destructure of a map or a list. Every one of those has a
near-identical F# form. The syntax shifts — case … do … end becomes
match … with, the ;-separated clauses become
|-prefixed arms, and a list's head/tail splits with :: rather
than | — but the shape, the clause order, and the wildcard _
are the same. This dive is the tour of that familiar ground, so the two genuinely new things — exhaustiveness
and active patterns — land on a base you already have.
The tagged tuple, side by side
Start with the match an Elixir developer writes most: a case over an
{:ok, v} / {:error, _} tuple. F# expresses the same thing as
a match over a Result — Ok v for the
success case, Error _ for the failure. The tag becomes a constructor; the bind on
v and the wildcard on the error are unchanged.
Elixir — case over a tagged tuple
# Elixir case result do {:ok, v} -> v {:error, _} -> 0 end
F# — match over a Result (verified with dotnet fsi)
let describe result = match result with | Ok v -> v | Error _ -> 0 // describe (Ok 42) -> 42 // describe (Error "boom") -> 0
- {:ok, v} → Ok v
- the tag
:okbecomes theResultcaseOk; the boundvis the same idea. - {:error, _} → Error _
:errorbecomesError; the wildcard_is identical in both languages.- case … do → match … with
- the construct keyword changes; the clause order and first-match-wins semantics do not.
Tuples, lists, records, guards
The rest of the Elixir matching vocabulary carries over the same way. A tuple destructures in the parameter
position; a list splits into head and tail; a record matches on its fields; a when
guard refines an arm. The four blocks below were each run through dotnet fsi.
Elixir — destructure, split, guard
# Elixir def swap({a, b}), do: {b, a} def sum([]), do: 0 def sum([h | t]), do: h + sum(t) def sign(n) when n > 0, do: "pos" def sign(0), do: "zero" def sign(_), do: "neg"
F# — the same, with verified FSI results
let swap (a, b) = (b, a) // swap (1, 2) -> (2, 1) let rec sum xs = match xs with | [] -> 0 | h :: t -> h + sum t // sum [1; 2; 3; 4] -> 10 let sign n = match n with | x when x > 0 -> "pos" | 0 -> "zero" | _ -> "neg" // sign 5, sign 0, sign -3 -> pos zero neg
A record matches like an Elixir struct or map: name the fields you care about, bind or test them, and let the
rest go. The arm below settles an order by its fields, the when guard handling the
zero-total case.
type Order = { Id: int; Total: decimal; Paid: bool } let status o = match o with | { Paid = true } -> "settled" | { Total = t } when t = 0M -> "free" | _ -> "owing" // status { Id = 1; Total = 19.99M; Paid = false } -> "owing"
::, where Elixir writes
[h | t] with a pipe. The pipe | in F# is the arm separator, so
the two roles never collide — h :: t is the split, | begins the
next arm.Matching in the function head
Elixir spreads a match across several function clauses, one per shape. F# folds the same idea into one
binding with the function keyword — a shorthand for a lambda that immediately matches
its single argument. The arms are the function's clauses.
Elixir — one clause per head
# Elixir def classify(0), do: "zero" def classify(n) when n > 0, do: "pos" def classify(_), do: "neg"
F# — the function keyword (verified)
let classify = function | 0 -> "zero" | n when n > 0 -> "pos" | _ -> "neg" // classify 7 -> "pos"
There is nothing here to unlearn. The Elixir match vocabulary maps almost word for word: the construct keyword changes, the cons operator changes, and the rest is the same matching you already do.
Run a match in both languages
Pick a value. It runs through describe — the
{:ok, v} / {:error, _} match from part 1 — in both
Elixir and F#. The diagram lights the arm that fires; the readout shows what each language returns, and that
the two answers agree. The arm that fires is decided by a pure function over a fixed set of inputs, so the
static table below holds every answer with no JavaScript.
describe value · which arm fires
| value | arm that fires | Elixir / F# both return |
|---|---|---|
| Ok 42 | | Ok v -> v | 42 |
| Ok 7 | | Ok v -> v | 7 |
| Error "boom" | | Error _ -> 0 | 0 |
| Error "not found" | | Error _ -> 0 | 0 |
The same value, the same arm, the same answer in both languages. The match is the shared vocabulary; only the keywords differ.
What the compiler does that the BEAM does not
So far the only difference is spelling. The change that matters comes from the type system underneath. In
Elixir the clauses of a case are checked when a value reaches them; a missing clause
is found at run time. In F# the type carries the full set of cases a Result or a
union can take, so the compiler can tell — before the program runs — whether a match
covers all of them. That check is the next dive.
Elixir habit
case … do … end over a tagged tuple, clauses checked when a value arrives at run time.
F# form
the same match … with, plus a compile-time check that the arms cover every case the type can take.
Where this leads
The match crosses unchanged; the checking is what is new. The next dive shows the compiler reporting a
missing case — the FS0025 warning — where Elixir would wait for a
CaseClauseError at run time. The dive after extends the pattern language itself with
active patterns.
References
Sources
- Microsoft — F# documentation — pattern matching, the
matchexpression and thefunctionkeyword. - FSharp.Core API reference — the
Resulttype and list operators matched against here. - Microsoft — .NET documentation — the runtime and types underneath F# matching.
Related in this course
- C3.03.2 — Exhaustiveness — the first power F# adds to the match toured here.
- Elixir — The match operator —
=as assertion,case, and destructuring on the BEAM. - C1.05 — Pattern matching & active patterns — the same constructs in language depth.