Dive C2.09.3 · the lab
Ship it
One file, in the right order, with an entry point — and the plan for Monday.
The core and the shell exist; this dive puts them in one runnable program. F# evaluates a file
top to bottom, so the order is forced and it is also the order that reads well: the
types first, the pure core next, the shell after, and an
[<EntryPoint>] main last (C2.01's "declaration order
matters"). The whole file below compiles clean under dotnet build with warnings as
errors and runs to a known output. The dive closes with the one-paragraph plan for adopting F# in a C# codebase,
and a checklist of the chapter's eight translations to run against your own code.
Types, then core, then shell, then main
A name must be declared before it is used, so the file reads in dependency order from the top. The types
come first because everything refers to them; the pure core comes next because the shell calls it; the shell
comes after; and main is last because it drives the lot. The diagram shows the four
bands of one Program.fs.
Program.fs · one file, four bands, top to bottom
- 1 · types
Tier,Customer— referred to by everything below, so first.- 2 · core
discount,totalDiscount— pure; called by the shell.- 3 · shell
parseTier,load,render— the effects, around the core.- 4 · main
[<EntryPoint>]— drives the program; last, because it uses all the above.
The whole program, in one screen
Here is the file. It is the four bands above, written out: the domain types, the pure core from C2.09.1,
the shell from C2.09.2, and an [<EntryPoint>] main that
loads and renders. It compiles with dotnet build under
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> — zero warnings, zero
errors — and running it prints the three lines shown at the bottom.
module Discounts // ---- 1 · types: the domain, first ---- type Tier = Bronze | Silver | Gold type Customer = { Id: int; Name: string; Tier: Tier } // ---- 2 · the pure core ---- let discount tier = match tier with | Bronze -> 0.0 | Silver -> 0.05 | Gold -> 0.10 let totalDiscount custs = custs |> List.sumBy (fun c -> discount c.Tier) // ---- 3 · the shell ---- let parseTier (s: string) = match s.Trim().ToLowerInvariant() with | "bronze" -> Some Bronze | "silver" -> Some Silver | "gold" -> Some Gold | _ -> None let load () = [ { Id = 1; Name = "Ann"; Tier = Gold } { Id = 2; Name = "Ben"; Tier = Bronze } ] let render custs = for c in custs do printfn "%s (%A): %.0f%%" c.Name c.Tier (discount c.Tier * 100.0) printfn "total discount: %.2f" (totalDiscount custs) // ---- 4 · the entry point ---- [<EntryPoint>] let main argv = let custs = load () render custs 0 // $ dotnet build -> Build succeeded. 0 Warning(s) 0 Error(s) // $ dotnet run // Ann (Gold): 10% // Ben (Bronze): 0% // total discount: 0.10
The parseTier function is in the file because the shell needs it for real
input; the main here calls load directly, since the literal
already holds typed Customer values. Swap load for a file read
and parseTier sits exactly where the string crosses in.
Where to start on Monday
F# does not arrive as a rewrite. The smallest first step is one module: put a slice of the domain in discriminated unions and records, write the rules over it as pure functions, and call the existing C# at the seam — F# consumes the BCL and your own assemblies directly (C2.08), so the new core can sit beside the old code and be called from it. The pure core is where the value lands first, because it is the part that tests without setup and reads without surprises. The shell can stay in C# at the start; over time, more of the domain moves into the typed core.
Monday morning
A C# solution with a thick domain in classes and a lot of null and switch.
The first F# module
One slice of the domain as DUs and records, its rules as pure functions, called from the existing C# at the seam — a functional core that grows.
The ship checklist · the chapter's eight translations
Chapter C2 translated eight C# habits into their idiomatic F# form. Tick each as you apply it to your own code; the readout names the F# form and the count tracks how many of the eight are in place. The list is also the review pass for a first F# module — when all eight are ticked, the C# habits have a typed home.
eight habits · tick each as you apply it
Eight translations, one shape. A C# team that ticks the list has a pure core in typed data and a thin shell around it — F# in a real solution, one module at a time.
Where this leads
This closes chapter C2: a C# developer can now read and write idiomatic F#, and ship it in the functional-core, imperative-shell shape. The next chapters leave the translation behind — C3 builds data structures and algorithms in F#, and C4 lands the language on a real DevOps tool written in F#.
References
Sources
- Microsoft — F# documentation — program structure, modules, and the
[<EntryPoint>]attribute. - Microsoft — F# style guide — the file and declaration order, and organising a solution.
- Microsoft — .NET documentation —
dotnet build/dotnet runand project files.
Related in this course
- C2.09.1 — The functional core — the pure middle of the program above.
- C2 — F# for C# developers — the eight translations the checklist recaps.
- C1.06 — Discriminated unions & records — the two type forms the core is built from.