Module C2.09 · the chapter lab
Mixing paradigms in one solution
A functional core, an imperative shell — the shape a C# team can adopt tomorrow.
F# is not all-or-nothing, and the move to it is not a rewrite. The pragmatic shape — the one a
C# team can put into a real project on day one — is a functional core, imperative shell. Model
the domain with records and discriminated unions so illegal states cannot be built; compute with
pure functions and folds that take input and return output with no side effects; and keep I/O,
logging, and any mutation in a thin outer layer at the edges. The eight translations of this
chapter — classes to modules, mutable to immutable, null to
Option, inheritance to discriminated unions, loops to fold,
interfaces to functions, LINQ to seq, and interop at the seam — all land in this one
shape. This lab walks one small worked solution, a tiered-discount calculator, end to end, then names where to
start on Monday.
One shape, assembled from the chapter
The earlier modules each translated a single C# habit. This lab puts them together. The same
tiered-discount example runs through all three dives, and each dive lands a different layer of the
functional core, imperative shell architecture. The core holds the domain types and the pure
functions over them; the shell holds the reading, writing, and contained mutation that a real program needs;
and the third dive assembles both into one runnable file with an [<EntryPoint>]
main.
The first dive builds the core: a Tier discriminated union and a
Customer record, a discount function defined by a
match over the tier, and a totalDiscount that folds the list
with List.sumBy. The core is pure — same input, same output, no side effects — so a C#
team can unit-test it trivially. The second dive builds the shell: a thin outer layer that
parses input at the seam (returning Option, the C2.03 / C2.08 boundary), calls the
pure core, and renders, with any mutation kept local to one function (the C2.02 "mutation, contained" rule).
The third dive is the lab itself: the file and module order, the
main, and the one-paragraph plan for adopting F# in an existing C# codebase.
The three dives · select one
Model the domain with records and discriminated unions so illegal states cannot be built, then compute with pure functions and folds — the part a C# team can unit-test with no setup.
Open this dive · or pick another above
The core is where the value is — pure functions over typed data — and the shell is the thin band that lets it talk to the world. A C# team adopts the shape one module at a time.
The worked example, in one screen
The lab carries one example through all three dives: a tiered-discount calculator. A
Customer has a loyalty Tier — Bronze,
Silver, or Gold — and each tier earns a discount rate. The
pure core is below: the C# the reader knows, beside its idiomatic F#. Both compute the same total over the same
two customers. Every // val it : echo was produced by dotnet fsi
9.0.303; the C# was verified with dotnet-script.
C# — a record and a switch expression
record Customer(int Id, string Name, string Tier); double Discount(string tier) => tier switch { "Gold" => 0.10, "Silver" => 0.05, _ => 0.0, // the default arm is mandatory — any string is legal }; Discount("Gold"); // 0.1
F# — a discriminated union and a total match
type Tier = Bronze | Silver | Gold type Customer = { Id: int; Name: string; Tier: Tier } let discount tier = // val discount : Tier -> float match tier with | Bronze -> 0.0 | Silver -> 0.05 | Gold -> 0.10 // add a tier and every match warns FS0025 let customers = [ { Id = 1; Name = "Ann"; Tier = Gold } { Id = 2; Name = "Ben"; Tier = Bronze } ] let totalDiscount custs = custs |> List.sumBy (fun c -> discount c.Tier) totalDiscount customers // val it : float = 0.1
The C# starting point
An object host with records and switch — open over its inputs, and any string is a legal Tier.
The F# shape
A pure core of records and discriminated unions — the tier is a closed set, the math is a total match, and the whole thing is a value-in, value-out function.
The three dives
Read them in order. The first builds the pure core, the second wraps it in an effectful shell, and the
third assembles both into one runnable program and closes with the chapter's eight-step adoption checklist.
Every F# snippet across the three is a verified echo from this chapter — the full
[<EntryPoint>] program in the third dive compiles clean under
dotnet build with warnings as errors.
The functional core
Records and discriminated unions for the domain, pure functions and a fold for the math — the part a C# team can unit-test trivially.
- pure
Tier,Customer,discount,totalDiscount
The imperative shell
A thin outer layer: parse at the seam, call the pure core, render — with any mutation kept local to one function.
- effectfulI/O and contained mutation at the edges
Ship it (the lab)
Assemble the pieces into one runnable program, then the eight-step plan for adopting F# in an existing C# codebase.
- assembledfile order,
[<EntryPoint>]main, the checklist
References
Sources
- Microsoft — F# documentation — records, discriminated unions, and program structure in F#.
- Microsoft — F# style guide — the conventions for organising types, the pure core, and the shell.
- FSharp.Core API reference —
List.sumByand the collection folds the core is built on.
Related in this course
- C2.09.1 — The functional core — the pure domain model and its math.
- C2.02 — Immutability & expressions — the immutable core and the "mutation, contained" rule the shell follows.
- C1.06 — Discriminated unions & records — the two type forms the core is built from, in language depth.