jonnifyfsharp

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.

C3.02.3 · part 1

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).
In Elixir, adding a new tag the old 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.
C3.02.3 · part 2

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_change path. 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.
F#'s concurrency answer to a 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 Elixir · dynamism compile-time run-time
shape errors · favours F# · F#: a missing case stops compiling (FS0025) · Elixir: a FunctionClauseError when the value arrives
Without JavaScript: the whole trade, both columns
F# static types buyElixir's dynamism gave
shape errors caught at compile timehot code reload (OTP code_change)
inferred, typed contractsmacros / metaprogramming (defmacro, quote)
refactors the compiler propagatesno type ceremony while a design is in flux
exhaustive match checksone 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.

C3.02.3 · part 3

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

Related in this course