jonnifyfsharp

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

C2.09.1 the functional core C2.09.2 the imperative shell C2.09.3 ship it (the lab) pure effectful assembled
C2.09.1 · The functional core

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.

You build the Tier union, the Customer record, discount, and totalDiscount

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.

live module hub built dive ready soon specified

References

Sources

Related in this course