Module C3.02
From dynamic to static types
The one part of F# that is genuinely new coming from Elixir.
Most of what an Elixir developer knows carries straight over to F# — immutability,
pattern matching, the |> pipe. The type system is the exception. In Elixir, a value
carries its type at run time, and the shape of a function's input is checked by a pattern match when the call
runs: def add(a, b), do: a + b takes anything. In F#, every value has a static type
— but you rarely write one. The Hindley–Milner inference algorithm derives the type from how a value is used, and
fixes it before the program runs: let add a b = a + b is
int -> int -> int, checked. This module takes that shift in three dives —
inference, illegal states made unrepresentable, and an even-handed look at the trade.
Run time, or compile time
The difference is one of when. Elixir settles a value's type when the code runs, with the value in hand; F# settles it when the code compiles, with a type it inferred. Both languages check shape — F# moves the check earlier, and into the type. The three dives below take that one at a time, then weigh the cost on both sides.
The first dive is type inference: F# is statically typed, yet you annotate almost nothing, because inference works the type out from use. The second is making illegal states unrepresentable: where Elixir models variants with tagged tuples and atoms held together by convention, an F# discriminated union ties data so the nonsense state has no value to build. The third is the trade — what the static types buy, and what Elixir's dynamism gave in return.
The three dives · select one
F# is statically typed, yet you rarely annotate: let add a b = a + b infers val add: a: int -> b: int -> int. Where Elixir checks shape at run time, the type is settled at compile time.
Open this dive · or pick another above
The shift is not "now you write types". It is that the type is settled before the program runs, and the compiler reads off it the checks Elixir would have run later.
The shift, in one line each
The contrast is concrete. Where Elixir takes any term and checks it when the call runs, F# infers a type and checks it when the code compiles. Where Elixir tags a tuple by convention, F# ties the data into a union case the compiler enforces. The same two examples run through all three dives.
Elixir — a value carries its type at run time
# Elixir — checked when it runs, not before def add(a, b), do: a + b # takes anything; shape settled at run time {:paid, 42} # a tagged tuple; the tag is a convention :pending # an atom; one of a set held by discipline
F# — the type is inferred, and settled at compile time
let add a b = a + b // val add: a: int -> b: int -> int type Payment = Unpaid | Paid of decimal // an "amount" without "paid" has no value to build
Elixir
Shape is checked when the code runs, by the pattern match in front of the value — a tag or an atom carries meaning the code agrees to honour.
F#
Shape is checked when the code compiles, against a type the compiler inferred — the meaning is in the type, and a state outside it cannot be written.
The three dives
Read them in order. Each stands on valid F# beside real, idiomatic Elixir: every
// val echo below was produced by dotnet fsi, and the Elixir
is the everyday form — tagged tuples, atoms, and the case match. Together they take an
Elixir developer from "shape is a run-time match" to "shape is an inferred, enforced type", and weigh what each
regime costs.
Type inference
Statically typed, yet you rarely annotate — Hindley–Milner derives the type from use and fixes it.
- inferreda static type with no annotation, plus the value-restriction trap
Making illegal states unrepresentable
A discriminated union ties the data so {:unpaid, 42} has no value to build.
- modelledthe DU vs the bool-soup record, and the exhaustiveness backstop
The trade
What the types buy — and what Elixir's dynamism gave: hot reload, macros, less ceremony.
- weighedcompile-time guarantees beside run-time power, even-handed
References
Sources
- Microsoft — F# documentation — type inference, discriminated unions, and pattern matching in F#.
- F# Core Library reference — the core types inference settles on.
- The F# Software Foundation — the language's home and community.
Related in this course
- C3.02.1 — Type inference — the compiler infers, you rarely annotate.
- C3 — F# for Elixir developers — the chapter this module sits in.
- Elixir F3.01 — Values, types & IEx — how the BEAM types a value, dynamically.
- Elixir F2.07 — Algebraic data types — tagged tuples and atoms, the model F# types.
- C1.06 — Discriminated unions & records — illegal states unrepresentable, in language depth.