jonnifyfsharp

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.

C2.09.3 · part 1

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 type Tier = Bronze | Silver | Gold ; type Customer = { … } 2 · the pure core let discount tier = … ; let totalDiscount custs = … 3 · the shell let parseTier s = … ; let load () = … ; let render custs = … 4 · the entry point [<EntryPoint>] let main argv = load () |> render ; 0
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.
C2.09.3 · part 2

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.

C2.09.3 · part 3

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

Applied 0 / 8
0 of 8 applied · tick a habit to see its F# form — or tick all eight for the full chapter.

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

Related in this course