jonnifyfsharp

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.

C3.03.1 · part 1

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 :ok becomes the Result case Ok; the bound v is the same idea.
{:error, _} → Error _
:error becomes Error; 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.
C3.03.1 · part 2

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"
F# splits a list with the cons operator ::, 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.
C3.03.1 · part 3

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 match … with returns Ok 42 which arm ? Ok v · / · Error _ | Ok v -> v binds v, returns it | Error _ -> 0 ignores the reason, returns 0
Elixir case (Ok 42) → 42 ·  F# describe (Ok 42) → 42 · arm | Ok v -> v
Without JavaScript: the same four values, by hand
valuearm that firesElixir / F# both return
Ok 42| Ok v -> v42
Ok 7| Ok v -> v7
Error "boom"| Error _ -> 00
Error "not found"| Error _ -> 00

The same value, the same arm, the same answer in both languages. The match is the shared vocabulary; only the keywords differ.

C3.03.1 · what changes next

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

Related in this course