Dive C3.02.3
The trade — both sides, honestly
A static type system is not a free upgrade. It buys, and it charges.
It would be easy to read this module as "F# is the grown-up version of Elixir." That is not the case, and it is worth saying plainly. Static types buy real guarantees — a class of run-time shape errors that stops compiling, signatures that are inferred contracts, and refactors the compiler propagates for you. Elixir's dynamism bought real power — hot code reload on a running system, metaprogramming that whole frameworks are built on, and the freedom to move before the types are settled. The move to F# is a trade, made with open eyes, not a conversion. This dive weighs both sides.
What the static types buy
The gains are concrete. A value that might be missing is Option; a payment that
might be unpaid has no amount slot. A signature is inferred and checked, so it documents the function without
a comment. And when a type changes, the compiler hands you the exact list of sites to fix.
Signatures are inferred contracts
let parsePort (s: string) = match System.Int32.TryParse s with | true, n -> Ok n | false, _ -> Error (sprintf "not a port: %s" s) // val parsePort: s: string -> Result<int,string> parsePort "8080" // val it: Result<int,string> = Ok 8080 parsePort "nope" // val it: Result<int,string> = Error "not a port: nope"
Refactors propagate
Add a case to a union and every match that does not handle it stops compiling. The
compiler names the missing case — the same FS0025 seen in the last dive, now working
as a refactor tool.
type Shape = | Circle of float | Rect of float * float | Tri of float * float * float // new case added let area s = match s with | Circle r -> 3.14 * r * r | Rect (w, h) -> w * h // warning FS0025: Incomplete pattern matches on this expression. // For example, the value 'Tri (_, _, _)' may indicate a case not covered by the pattern(s).
case does not handle is found by a
test, or by a FunctionClauseError in production. In F#, the compiler walks every
match over the type and reports each one that is now incomplete.What Elixir's dynamism gave
The dynamism was not a deficiency to be corrected. It paid for capabilities F# does not match — and a team coming from the BEAM gives them up knowingly, not by accident.
- hot code reload
- the BEAM swaps a module's code in a running system without dropping connections — the OTP
code_changepath. F# on the CLR has no equivalent for live upgrades; a new version is a redeploy. - metaprogramming
- Elixir macros (
defmacro,quote/unquote) run at compile time over the AST, and Ecto, Phoenix, and ExUnit are built on them. F# has quotations and type providers, but the everyday "write a macro to remove this boilerplate" move is an Elixir habit, not an F# one. - less ceremony
- a dynamic language asks for no type when shapes are in flux. Early in a design, Elixir's
{:ok, _}/{:error, _}and bare maps let an idea move before its types are settled.
GenServer is the
MailboxProcessor, covered in module C3.07. It gives the actor and the mailbox; OTP's
supervision trees and live code_change upgrades are the part that does not carry over.Weigh the trade
Pick a concern. The balance tips to the side that holds the advantage, and the readout names what each regime offers for it — the F# gain beside the Elixir one. Some concerns favour the types; some favour the dynamism; the point is that neither side is empty.
a concern · which side it favours
| F# static types buy | Elixir's dynamism gave |
|---|---|
| shape errors caught at compile time | hot code reload (OTP code_change) |
| inferred, typed contracts | macros / metaprogramming (defmacro, quote) |
| refactors the compiler propagates | no type ceremony while a design is in flux |
| exhaustive match checks | one model fits any term |
The trade is real on both sides. F# moves a class of bugs to compile time and a class of documentation into signatures; Elixir keeps a class of run-time power that types cannot express.
The honest summary
Coming from the BEAM, the gain is the compiler as a second pair of hands on every refactor, and a class of shape bug that stops compiling. The cost is the ceremony of a type where an Elixir map would have done, and the loss of live upgrades and everyday macros. Both columns are real; the right choice depends on the system, not on which language is "better".
F# static types buy | Elixir's dynamism gave ---------------------------+--------------------------- shape errors at compile | hot code reload (OTP) inferred typed contracts | macros / metaprogramming refactors the compiler | no type ceremony while propagates | a design is in flux exhaustive match checks | one model fits any term
Elixir kept
Run-time power — live code_change upgrades, AST macros, and the freedom to move before the types are settled.
F# gained
Compile-time guarantees — shape errors that do not compile, inferred contracts, and refactors the compiler walks for you.
Where this leads
The type system is the one genuine shift coming from Elixir, and this module weighed it whole. From here, C3
returns to the ground that carries over: pattern matching sharpened with exhaustiveness, the
|> pipe that is identical in both languages, and the
MailboxProcessor that answers the GenServer.
References
Sources
- Microsoft — F# documentation — type inference, discriminated unions, quotations, and type providers.
- The F# Software Foundation — the language's design goals and trade-offs.
- Microsoft — .NET documentation — the CLR's deployment model, and why live code-swap differs from the BEAM.
Related in this course
- C3 — F# for Elixir developers — the chapter this module closes a thread in.
- Elixir F3.01 — Values, types & IEx — the dynamic regime this dive weighs.
- Elixir F2.07 — Algebraic data types — the tagged-tuple modelling the types replace.
- C1.06 — Discriminated unions & records — the construct doing the buying, in depth.