Intermediate
This intermediate tutorial covers F#'s production-ready patterns and advanced functional features through 30 heavily annotated examples. Topics include async workflows, computation expressions, type providers, active patterns, and object programming within F#'s functional-first paradigm.
Example 31: Async Workflows - Basic Asynchrony
Async workflows enable non-blocking I/O and concurrency using F#'s async/let! syntax, similar to async/await in C#.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
A[async block]:::blue --> B[async.Sleep 1000]:::orange
B --> C[let! waits<br/>non-blocking]:::teal
C --> D[return value]:::orange
style A fill:#0173B2,stroke:#000,color:#fff
style B fill:#DE8F05,stroke:#000,color:#fff
style C fill:#029E73,stroke:#000,color:#fff
style D fill:#DE8F05,stroke:#000,color:#fff
Code:
// Example 31: Async Workflows - Basic Asynchrony
let asyncOperation = async {
// => async { ... } creates async computation
// => Type: Async<string>
printfn "Starting operation..."
// => Executes synchronously
do! Async.Sleep 1000 // => do! awaits async computation (non-blocking)
// => Sleeps for 1000ms without blocking thread
printfn "Operation completed"
// => Executes after sleep completes
return "Result" // => return produces async result
} // => Workflow is NOT executed yet (lazy)
let result = asyncOperation |> Async.RunSynchronously
// => Async.RunSynchronously executes workflow
// => BLOCKS current thread until completion
// => result is "Result" (type: string)
// => Outputs: Starting operation... (1s delay) Operation completed
printfn "%s" result // => Outputs: ResultKey Takeaway: Async workflows use async { ... } with let!/do! for awaiting. Async.RunSynchronously executes the workflow.
Why It Matters: Async workflows enable efficient I/O operations without blocking threads, allowing a single thread pool to handle thousands of concurrent connections. In ASP.NET Core services, replacing synchronous database calls with async { let! result = db.QueryAsync() ... } dramatically increases throughput under load. Thread-per-request models hit OS thread limits around 1,000-2,000 concurrent requests; async workflows scale to tens of thousands on the same hardware.
Example 32: Async Parallel Execution
Async.Parallel executes multiple async operations concurrently, improving throughput for independent tasks.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
A[Start]:::blue --> B[Task 1: 500ms]:::orange
A --> C[Task 2: 500ms]:::orange
A --> D[Task 3: 500ms]:::orange
B --> E[All complete<br/>~500ms total]:::teal
C --> E
D --> E
style A fill:#0173B2,stroke:#000,color:#fff
style B fill:#DE8F05,stroke:#000,color:#fff
style C fill:#DE8F05,stroke:#000,color:#fff
style D fill:#DE8F05,stroke:#000,color:#fff
style E fill:#029E73,stroke:#000,color:#fff
Code:
// Example 32: Async Parallel Execution
let task1 = async { // => First async task (type: Async<string>)
do! Async.Sleep 500 // => Simulates 500ms I/O without blocking thread
return "Task 1" // => Returns result string
}
let task2 = async { // => Second async task (independent of task1)
do! Async.Sleep 500 // => Also 500ms I/O
return "Task 2" // => Returns "Task 2" after sleep
}
let task3 = async { // => Third async task (independent)
do! Async.Sleep 500 // => Also 500ms I/O
return "Task 3" // => Returns "Task 3" after sleep
}
let parallelExecution = async {
// => Outer async workflow combining tasks
let! results = [task1; task2; task3]
|> Async.Parallel
// => Async.Parallel runs all tasks concurrently
// => Returns Async<string[]>
// => let! awaits all tasks to complete
// => Total time: ~500ms (NOT 1500ms sequential)
return results // => results is [|"Task 1"; "Task 2"; "Task 3"|]
}
let allResults = parallelExecution |> Async.RunSynchronously
// => Async.RunSynchronously blocks current thread
// => allResults is string array with 3 elements
printfn "%A" allResults // => Outputs: [|"Task 1"; "Task 2"; "Task 3"|]Key Takeaway: Async.Parallel executes async computations concurrently. Total time equals longest task, not sum of all tasks.
Why It Matters: Parallel async operations dramatically improve throughput for I/O-bound workloads where tasks don't depend on each other. Web scrapers fetch multiple pages concurrently using Async.Parallel, achieving near-linear speedup with task count. Microservices calling multiple downstream APIs in parallel reduce response latency from sum-of-calls to max-of-calls. Semaphore controls limit concurrency to respect rate limits while maintaining parallel execution where permitted.
Example 33: Task Expressions (F# 6.0+)
Task expressions provide direct interop with .NET Task-based APIs using task { ... } computation expression.
// Example 33: Task Expressions (F# 6.0+)
open System.Threading.Tasks
let taskOperation = task {
// => task { ... } creates Task<'T>
// => Directly compatible with C# async/await
printfn "Starting task..."
// => Executes synchronously
do! Task.Delay(1000) // => do! awaits Task (not Async)
// => Task.Delay is .NET task-based delay
printfn "Task completed"
return 42 // => return produces Task<int>
} // => Type: Task<int>
let result = taskOperation.Result
// => .Result blocks until task completes
// => result is 42
// => Outputs: Starting task... (1s delay) Task completed
printfn "%d" result // => Outputs: 42
// Task composition:
let composedTask = task {
let! x = taskOperation
// => let! awaits Task<int>
// => x is 42
let! y = task { return x * 2 }
// => Nested task expression
// => y is 84
return x + y // => Returns 126
}
let composedResult = composedTask.Result
// => composedResult is 126
printfn "%d" composedResult
// => Outputs: 126Key Takeaway: Use task { ... } for .NET Task interop. It's faster than async { ... } for Task-based APIs.
Why It Matters: Task expressions eliminate the overhead of converting between F# Async<'T> and .NET Task<'T>, which requires allocation and scheduling costs at each boundary. ASP.NET Core, Entity Framework, and gRPC all use Task-based APIs natively. Migrating hot paths from async to task expressions can reduce allocation pressure in high-throughput scenarios. Most new F# projects targeting ASP.NET Core prefer task for direct framework compatibility.
Example 34: Computation Expressions Basics - Maybe Builder
Computation expressions enable custom control flow syntax. The "maybe" builder handles Option types declaratively.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
A[maybe builder]:::blue --> B{let! Some v?}:::orange
B -->|Some| C[Continue with v]:::teal
B -->|None| D[Short-circuit: None]:::orange
C --> E[return Some result]:::teal
style A fill:#0173B2,stroke:#000,color:#fff
style B fill:#DE8F05,stroke:#000,color:#fff
style C fill:#029E73,stroke:#000,color:#fff
style D fill:#DE8F05,stroke:#000,color:#fff
style E fill:#029E73,stroke:#000,color:#fff
Code:
// Example 34: Computation Expressions Basics - Maybe Builder
type MaybeBuilder() = // => Computation expression builder for Option
member _.Bind(x, f) =// => Bind: handles let! syntax
// => x: option<'a>, f: 'a -> option<'b>
match x with
| Some v -> f v // => Some: continue with unwrapped value
| None -> None // => None: short-circuit entire computation
member _.Return(x) = // => Return: handles return syntax
Some x // => Wraps value in Some
let maybe = MaybeBuilder()
// => Create builder instance
let divide x y = // => Safe division returning option
if y = 0 then None // => Division by zero: None
else Some (x / y) // => Valid division: Some result
let computation = maybe {
let! a = divide 10 2 // => a is 5 (10/2, unwrapped from Some)
// => If divide returned None, entire computation returns None
let! b = divide 20 4 // => b is 5 (20/4)
let! c = divide 30 a // => c is 6 (30/5)
return a + b + c // => Returns Some 16 (5+5+6)
} // => computation is Some 16
printfn "%A" computation // => Outputs: Some 16
let failedComputation = maybe {
let! a = divide 10 2 // => a is 5
let! b = divide 20 0 // => divide returns None (div by zero)
// => Computation short-circuits here
return a + b // => Never reached
} // => failedComputation is None
printfn "%A" failedComputation
// => Outputs: NoneKey Takeaway: Computation expressions provide custom control flow. let! unwraps values, return wraps results. Builders define semantics.
Why It Matters: Computation expressions eliminate nested match statements that grow exponentially with each additional optional step in a workflow. Railway-oriented programming uses builders to chain operations that may fail - each let! value = optionalOp() automatically short-circuits on failure. Real payment processing flows validate card, check balance, authorize, and debit in a single clean expression chain, with any failure returning an error without explicit branching at each step.
Example 35: Sequence Expressions
Sequence expressions (seq { ... }) generate lazy sequences using yield syntax, similar to C# iterators.
// Example 35: Sequence Expressions
let numbers = seq { // => seq { ... } creates lazy sequence
yield 1 // => yield produces single element
// => Element NOT computed until consumed
yield 2
yield 3
} // => Type: seq<int> (IEnumerable<int>)
printfn "%A" (numbers |> Seq.toList)
// => Forces evaluation: [1; 2; 3]
// Range syntax:
let range = seq { 1 .. 10 }
// => Lazy range from 1 to 10
// => Equivalent to Seq.init
printfn "%A" (range |> Seq.take 5 |> Seq.toList)
// => Takes first 5: [1; 2; 3; 4; 5]
// yield! for sub-sequences:
let combined = seq { // => Combining sequences
yield 0 // => Single element: 0
yield! [1; 2; 3] // => yield! flattens sequence
// => Yields 1, then 2, then 3
yield! seq { 4 .. 6 }// => Yields 4, 5, 6
yield 7 // => Single element: 7
} // => combined is [0; 1; 2; 3; 4; 5; 6; 7] (lazy)
printfn "%A" (combined |> Seq.toList)
// => Outputs: [0; 1; 2; 3; 4; 5; 6; 7]
// Conditional yield:
let evens = seq { // => Conditional element generation
for i in 1 .. 10 do // => Loop through range
if i % 2 = 0 then // => Condition
yield i // => Yield only even numbers
} // => evens is [2; 4; 6; 8; 10] (lazy)
printfn "%A" (evens |> Seq.toList)
// => Outputs: [2; 4; 6; 8; 10]Key Takeaway: Sequence expressions use yield for elements, yield! for sub-sequences. Evaluation is lazy until consumption.
Why It Matters: Sequence expressions enable generator-style programming for large or infinite data sources, yielding elements on demand without materializing the entire collection. Log parsers yield filtered lines lazily, processing gigabyte log files with kilobytes of memory. Data pipeline generators produce batches of database records for processing without loading entire tables. The yield! syntax for sub-sequences enables composing generators hierarchically.
Example 36: Option Computation - Railway-Oriented Programming
Option-based computation expressions enable safe chaining of operations that might fail without explicit null checks.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph LR
A["Input strings"] -->|"tryParseInt"| B{"Parse OK?"}
B -->|"Some value"| C{"tryParseInt 2"}
B -->|"None"| Z["None (short-circuit)"]
C -->|"Some value"| D{"tryParseInt 3"}
C -->|"None"| Z
D -->|"Some value"| E{"tryDivide"}
D -->|"None"| Z
E -->|"Some result"| F["Some result"]
E -->|"None (div/0)"| Z
style A fill:#0173B2,stroke:#000,color:#fff
style B fill:#DE8F05,stroke:#000,color:#000
style C fill:#DE8F05,stroke:#000,color:#000
style D fill:#DE8F05,stroke:#000,color:#000
style E fill:#DE8F05,stroke:#000,color:#000
style F fill:#029E73,stroke:#000,color:#fff
style Z fill:#CC78BC,stroke:#000,color:#000
// Example 36: Option Computation - Railway-Oriented Programming
let tryParseInt (s: string) =
// => Parse string to int option (type: string -> int option)
match System.Int32.TryParse(s) with
| (true, value) -> Some value
// => Success: Some int (e.g., Some 10)
| (false, _) -> None // => Failure: None (e.g., for "abc")
let tryDivide x y = // => Safe division (type: int -> int -> int option)
if y = 0 then None // => Division by zero: None
else Some (x / y) // => Valid division: Some result
type OptionBuilder() = // => Builder for option computation expression chaining
member _.Bind(x, f) =// => Bind: implements let! syntax
match x with
| Some v -> f v // => Continue if Some, passing value to continuation
| None -> None // => Short-circuit: propagate None
member _.Return(x) = Some x
// => Return: wraps value in Some
let option = OptionBuilder()
// => Create builder instance for option { } syntax
let calculate input1 input2 input3 = option {
// => Chain of operations that might fail
let! num1 = tryParseInt input1
// => Parse first input; short-circuits if not valid int
let! num2 = tryParseInt input2
// => Parse second input; short-circuits if invalid
let! num3 = tryParseInt input3
// => Parse third input; short-circuits if invalid
let! sum = Some (num1 + num2)
// => Sum always succeeds (trivially wrapped in Some)
let! result = tryDivide sum num3
// => Divide sum by third number
// => Short-circuits with None if num3 is 0
return result // => Final result: Some result (success path)
}
let success = calculate "10" "20" "5"
// => 10 + 20 = 30, 30 / 5 = 6
// => success is Some 6
let parseFailure = calculate "abc" "20" "5"
// => First parse fails (tryParseInt "abc" = None)
// => parseFailure is None (short-circuit at num1)
let divZeroFailure = calculate "10" "20" "0"
// => Division by zero (tryDivide 30 0 = None)
// => divZeroFailure is None (short-circuit at result)
printfn "%A" success // => Outputs: Some 6
printfn "%A" parseFailure// => Outputs: None
printfn "%A" divZeroFailure
// => Outputs: NoneKey Takeaway: Option builders chain operations that may fail. First failure short-circuits entire computation, returning None.
Why It Matters: Railway-oriented programming eliminates defensive programming boilerplate by treating failure as a first-class concern in the type system. Payment processing pipelines validate card, check balance, authorize transaction, and debit account using option or result chains - any step returning None or Error automatically skips remaining steps. This makes error handling declarative and exhaustive, compared to try/catch chains that can accidentally swallow exceptions.
Example 37: Result Type - Explicit Error Handling
The Result type carries explicit error information instead of None, providing context for failures.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
A[Result type]:::blue --> B[Ok of value]:::teal
A --> C[Error of error]:::orange
style A fill:#0173B2,stroke:#000,color:#fff
style B fill:#029E73,stroke:#000,color:#fff
style C fill:#DE8F05,stroke:#000,color:#fff
Code:
// Example 37: Result Type - Explicit Error Handling
type Result<'T, 'TError> =
// => Generic result type (built-in in F# 4.1+, no need to define)
| Ok of 'T // => Success case carrying value of type 'T
| Error of 'TError // => Failure case carrying error of type 'TError
let tryParseInt (s: string) : Result<int, string> =
// => Returns Ok int or Error string (explicit signature)
match System.Int32.TryParse(s) with
| (true, value) -> Ok value
// => Success: Ok with parsed value (e.g., Ok 20)
| (false, _) -> Error (sprintf "Cannot parse '%s' as int" s)
// => Failure: Error with descriptive message (e.g., Error "Cannot parse 'abc' as int")
let tryDivide x y : Result<int, string> =
// => Safe division returning Result
if y = 0 then Error "Division by zero"
// => Error case with human-readable reason
else Ok (x / y) // => Success case with integer quotient
type ResultBuilder() = // => Builder class for result { } computation expression
member _.Bind(x, f) =// => Bind: handles let! syntax
match x with
| Ok v -> f v // => Continue pipeline with success value v
| Error e -> Error e
// => Short-circuit: propagate first error
member _.Return(x) = Ok x
// => Return: wraps value in Ok
let result = ResultBuilder()
// => Create builder instance
let calculate input1 input2 = result {
// => result { } computation expression
let! num1 = tryParseInt input1
// => Parse first input; short-circuits with Error if invalid
let! num2 = tryParseInt input2
// => Parse second input; short-circuits with Error if invalid
let! quotient = tryDivide num1 num2
// => Divide; short-circuits with Error if division by zero
return quotient // => Success path: return Ok quotient
}
let success = calculate "20" "4"
// => All steps succeed: 20/4 = 5
// => success is Ok 5
let parseError = calculate "abc" "4"
// => tryParseInt "abc" returns Error
// => parseError is Error "Cannot parse 'abc' as int"
let divZeroError = calculate "20" "0"
// => tryDivide 20 0 returns Error
// => divZeroError is Error "Division by zero"
printfn "%A" success // => Outputs: Ok 5
printfn "%A" parseError // => Outputs: Error "Cannot parse 'abc' as int"
printfn "%A" divZeroError// => Outputs: Error "Division by zero"Key Takeaway: Result type carries both success (Ok) and failure (Error) with context. Use Result for explicit error information.
Why It Matters: Result types provide structured error handling without exceptions, making error paths as explicit as success paths in the type system. Microservices return Result<Data, ApiError> with specific error codes like ValidationFailed, Unauthorized, or NotFound, enabling clients to handle each case programmatically. Unlike exceptions that bypass type checking, Result forces all callers to acknowledge possible failures at compile time, eliminating unhandled error scenarios.
Example 38: Type Providers - JSON
Type providers generate types from external schemas at compile time, providing IntelliSense for JSON structures. Type providers are a core F# compiler feature - the mechanism is built into the F# language. However, JsonProvider and CsvProvider implementations come from the community library FSharp.Data, which is not part of the standard library.
Why Not Core Features? The F# compiler provides the type provider mechanism as a language feature, but does not bundle pre-built providers for JSON/CSV. FSharp.Data is the canonical .NET community library implementing these providers. Install it with dotnet add package FSharp.Data or use #r "nuget: FSharp.Data" in F# scripts.
// Example 38: Type Providers - JSON
#r "nuget: FSharp.Data" // => Reference FSharp.Data NuGet package
// => Provides JsonProvider, CsvProvider, HtmlProvider
// => Required: not in F# standard library
open FSharp.Data // => Open FSharp.Data namespace
type WeatherData = JsonProvider<"""
{
"temperature": 22.5,
"humidity": 65,
"conditions": "sunny"
}
"""> // => JsonProvider generates types from sample JSON
// => Creates WeatherData type with typed properties
// => Compilation reads schema: temperature=float, humidity=int, conditions=string
// => IntelliSense available for all fields
let jsonString = """
{
"temperature": 18.3,
"humidity": 72,
"conditions": "cloudy"
}
""" // => JSON string to parse at runtime
let weather = WeatherData.Parse(jsonString)
// => Parse JSON string into typed WeatherData object
// => weather has compile-time type-checked properties
// => Runtime validates JSON matches schema
printfn "Temperature: %.1f°C" weather.Temperature
// => weather.Temperature is float (type-safe access)
// => Outputs: Temperature: 18.3°C
printfn "Humidity: %d%%" weather.Humidity
// => weather.Humidity is int (type-safe)
// => Outputs: Humidity: 72%
printfn "Conditions: %s" weather.Conditions
// => weather.Conditions is string (type-safe)
// => Outputs: Conditions: cloudy
// Array handling:
type WeatherArray = JsonProvider<"""
[
{ "city": "Jakarta", "temp": 28.5 },
{ "city": "Bandung", "temp": 22.0 }
]
"""> // => JsonProvider supports JSON arrays
// => Infers element types from sample array
let citiesJson = """
[
{ "city": "Jakarta", "temp": 29.1 },
{ "city": "Bandung", "temp": 21.5 },
{ "city": "Surabaya", "temp": 30.2 }
]
""" // => Actual data (different from sample)
let cities = WeatherArray.Parse(citiesJson)
// => cities is typed array of city objects
// => Each element has .City (string) and .Temp (float)
for city in cities do // => Iterate over typed array with IntelliSense
printfn "%s: %.1f°C" city.City city.Temp
// => Outputs: Jakarta: 29.1°C
// => Bandung: 21.5°C
// => Surabaya: 30.2°CKey Takeaway: JsonProvider generates types from sample JSON. Provides compile-time safety and IntelliSense for JSON data.
Why It Matters: Type providers eliminate manual DTO class definitions and runtime reflection overhead, generating compile-time types directly from data schemas. API clients using JsonProvider get immediate compile errors when upstream JSON schemas change, catching breaking changes in CI/CD before deployment. This dramatically reduces the feedback cycle for schema changes from "production alert" to "build failure", turning runtime errors into compile-time errors in data-intensive F# applications.
Note: Type providers are a core F# compiler feature, but JsonProvider/CsvProvider implementations come from the FSharp.Data community library. The F# standard library does not include pre-built providers for JSON/CSV - FSharp.Data is the canonical .NET library for these providers. Install with #r "nuget: FSharp.Data" in scripts or add the NuGet package to your project.
Example 39: Type Providers - CSV
CSV type provider generates strongly-typed accessors for CSV data with automatic type inference.
Why Not Core Features? CsvProvider is provided by the FSharp.Data community library, not the F# standard library. Type providers are a core F# compiler mechanism, but FSharp.Data implements the specific JSON/CSV/HTML/XML providers. Add #r "nuget: FSharp.Data" in scripts or <PackageReference Include="FSharp.Data" Version="6.x" /> in your project file.
// Example 39: Type Providers - CSV
open FSharp.Data // => FSharp.Data NuGet library (not standard library)
type StockData = CsvProvider<"""
Date,Symbol,Open,Close,Volume
2024-01-01,AAPL,180.50,182.30,50000000
2024-01-02,AAPL,182.00,181.50,48000000
"""> // => CsvProvider generates types from sample CSV
// => Infers types: Date=string, Open=float, Volume=int
// => Compile-time type generation from sample data
let csvData = """
Date,Symbol,Open,Close,Volume
2024-01-03,GOOGL,140.25,142.10,25000000
2024-01-04,GOOGL,142.50,141.80,23000000
2024-01-05,MSFT,380.00,385.50,30000000
""" // => Actual CSV data to parse at runtime
let stocks = StockData.Parse(csvData)
// => Parse CSV string into typed StockData collection
// => Returns object with .Rows property
for row in stocks.Rows do
// => Iterate over typed rows with IntelliSense
printfn "%s: %s - Open: %.2f, Close: %.2f, Volume: %d"
row.Date // => Type: string (inferred from sample column)
row.Symbol // => Type: string
row.Open // => Type: float (inferred from sample)
row.Close // => Type: float
row.Volume // => Type: int (inferred from sample)
// => Outputs:
// => 2024-01-03: GOOGL - Open: 140.25, Close: 142.10, Volume: 25000000
// => 2024-01-04: GOOGL - Open: 142.50, Close: 141.80, Volume: 23000000
// => 2024-01-05: MSFT - Open: 380.00, Close: 385.50, Volume: 30000000
// Aggregation with type safety:
let totalVolume = stocks.Rows
|> Seq.sumBy (fun row -> row.Volume)
// => Seq.sumBy: sum by key (type: int)
// => totalVolume is 78000000
let avgClose = stocks.Rows
|> Seq.averageBy (fun row -> row.Close)
// => Seq.averageBy: average by key (type: float)
// => avgClose is 156.466667
printfn "Total Volume: %d" totalVolume
// => Outputs: Total Volume: 78000000
printfn "Avg Close: %.2f" avgClose
// => Outputs: Avg Close: 156.47Key Takeaway: CsvProvider infers column types from sample data. Provides strongly-typed row access with IntelliSense.
Why It Matters: CSV type providers eliminate brittle string-index based access and manual type parsing, generating strongly-typed row accessors with IntelliSense support. Financial analysts processing trading data catch column name typos like row.Volumne at compile time instead of runtime. Teams integrating with data vendors benefit from automatic type inference: CsvProvider detects that Volume is integer and Open is float without manual schema definitions, reducing integration code significantly.
Example 40: Units of Measure
Units of measure add dimension checking to numeric types, preventing unit mismatch errors at compile time.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
A[float with unit]:::blue --> B[Compile-time checking]:::orange
B --> C[m + m = OK]:::teal
B --> D[m + kg = ERROR]:::orange
style A fill:#0173B2,stroke:#000,color:#fff
style B fill:#DE8F05,stroke:#000,color:#fff
style C fill:#029E73,stroke:#000,color:#fff
style D fill:#DE8F05,stroke:#000,color:#fff
Code:
// Example 40: Units of Measure
[<Measure>] type m // => Define meter unit
[<Measure>] type kg // => Define kilogram unit
[<Measure>] type s // => Define second unit
let distance = 100.0<m> // => 100 meters (type: float<m>)
// => Unit annotation: <unit>
let mass = 75.0<kg> // => 75 kilograms (type: float<kg>)
let time = 10.0<s> // => 10 seconds (type: float<s>)
let speed = distance / time
// => Division: m / s = m/s
// => speed is 10.0<m/s> (type: float<m/s>)
// => Derived unit automatically inferred
printfn "Speed: %.1f m/s" speed
// => Outputs: Speed: 10.0 m/s
// let invalid = distance + mass
// => COMPILE ERROR: Cannot add m and kg
// => Type mismatch caught at compile time
let totalDistance = distance + 50.0<m>
// => Adding same units: OK
// => totalDistance is 150.0<m>
// Force calculation (unit erased):
let energy = mass * (speed * speed)
// => kg * (m/s)^2 = kg·m²/s²
// => Type: float<kg m^2/s^2>
// => Dimensional analysis automatic
printfn "Kinetic energy: %.1f J" (float energy)
// => float casts away units
// => Outputs: Kinetic energy: 7500.0 J
// Generic units:
let double (x: float<'u>) : float<'u> =
// => Generic unit parameter 'u
// => Function preserves units
x * 2.0 // => Multiplying by unitless float preserves unit
let doubleDistance = double distance
// => doubleDistance is 200.0<m>
let doubleMass = double mass
// => doubleMass is 150.0<kg>
printfn "Double distance: %.1f m" doubleDistance
// => Outputs: Double distance: 200.0 mKey Takeaway: Units of measure prevent dimension errors. Define with [<Measure>] type, annotate with <unit>. Generic units use 'u.
Why It Matters: Units of measure can prevent catastrophic unit conversion errors at compile time - the Mars Climate Orbiter failure (pound-force vs. newtons) cost $328 million. Physics simulations and engineering calculations use units to eliminate conversion errors, with the compiler rejecting invalid operations like adding meters to kilograms. Aerospace, medical device, and financial systems use units of measure to guarantee dimensional consistency throughout calculation chains.
Example 41: Active Patterns - Single-Case
Single-case active patterns create custom pattern matching extractors, enabling declarative decomposition.
// Example 41: Active Patterns - Single-Case
let (|EmailParts|) (email: string) =
// => Single-case active pattern
// => (|PatternName|) defines pattern
// => Always succeeds (no None case, unlike partial patterns)
let parts = email.Split('@')
// => Split email at @ character
if parts.Length = 2 then
(parts.[0], parts.[1])
// => Return (username, domain) tuple
else
("", "") // => Invalid email: return empty strings
let analyzeEmail email = // => Function using active pattern
match email with
| EmailParts(user, domain) ->
// => EmailParts pattern extracts user and domain
// => Pattern ALWAYS matches (single-case)
sprintf "User: %s, Domain: %s" user domain
let result1 = analyzeEmail "john@example.com"
// => result1 is "User: john, Domain: example.com"
let result2 = analyzeEmail "invalid-email"
// => result2 is "User: , Domain: " (empty parts)
printfn "%s" result1 // => Outputs: User: john, Domain: example.com
printfn "%s" result2 // => Outputs: User: , Domain:
// Parameterized single-case pattern:
let (|Multiplied|) multiplier value =
// => Pattern with parameter (multiplier)
// => Takes multiplier, applies to value
value * multiplier // => Returns multiplied value (extracted value)
let describeMultiple value =
match value with
| Multiplied 2 doubled ->
// => Multiplied 2 applies: doubled = value * 2
// => doubled is bound to value*2 result
sprintf "Doubled: %d" doubled
| _ -> "" // => Other multipliers (not needed here)
let result3 = describeMultiple 5
// => 5*2 = 10
// => result3 is "Doubled: 10"
printfn "%s" result3 // => Outputs: Doubled: 10Key Takeaway: Single-case patterns use (|Name|) params. They always match and extract values for pattern matching.
Why It Matters: Single-case patterns enable domain-specific decomposition that makes complex parsing and extraction logic read declaratively. HTTP request parsers extract headers, query strings, and bodies using custom patterns, making routing code read like specifications: match request with | AuthenticatedRequest (user, body) -> processAuthorized user body. This abstraction hides parsing complexity behind patterns, improving code readability and enabling pattern reuse across multiple match expressions.
Example 42: Active Patterns - Multi-Case
Multi-case active patterns enable custom matching with multiple outcomes, like enhanced switch statements.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC
graph TD
A[match value]:::blue --> B{Active Pattern}:::orange
B -->|Even| C[Handle even]:::teal
B -->|Odd| D[Handle odd]:::purple
style A fill:#0173B2,stroke:#000,color:#fff
style B fill:#DE8F05,stroke:#000,color:#fff
style C fill:#029E73,stroke:#000,color:#fff
style D fill:#CC78BC,stroke:#000,color:#fff
Code:
// Example 42: Active Patterns - Multi-Case
let (|Even|Odd|) value = // => Multi-case active pattern: returns one of two cases
// => (|Case1|Case2|...|) syntax for multi-case
if value % 2 = 0 then Even
// => Even case (no associated data)
else Odd // => Odd case (no associated data)
let describe value = // => Function using custom Even/Odd pattern
match value with // => Exhaustive pattern match (only Even/Odd cases)
| Even -> // => Matches if (|Even|Odd|) returns Even
sprintf "%d is even" value
| Odd -> // => Matches if (|Even|Odd|) returns Odd
sprintf "%d is odd" value
printfn "%s" (describe 10)
// => Outputs: 10 is even
printfn "%s" (describe 7)
// => Outputs: 7 is odd
// Multi-case with data:
let (|Positive|Negative|Zero|) value =
// => Three-case pattern with associated data
if value > 0 then Positive value
// => Positive case carries original value
elif value < 0 then Negative -value
// => Negative case carries absolute value (negated)
else Zero // => Zero case (no associated data)
let describeNumber value =
match value with
| Positive v -> // => Extract positive value into v (e.g., v=42)
sprintf "+%d (positive)" v
| Negative v -> // => Extract absolute value into v (e.g., v=15 for -15)
sprintf "-%d (negative)" v
| Zero -> // => Zero case (no data to extract)
"0 (zero)"
printfn "%s" (describeNumber 42)
// => Outputs: +42 (positive)
printfn "%s" (describeNumber -15)
// => Outputs: -15 (negative)
printfn "%s" (describeNumber 0)
// => Outputs: 0 (zero)
// Parameterized multi-case:
let (|DivisibleBy|NotDivisibleBy|) divisor value =
// => Pattern with extra parameter (divisor)
if value % divisor = 0 then
// => Check if value is divisible by divisor
DivisibleBy (value / divisor)
// => Carries quotient as associated data
else
NotDivisibleBy // => Not divisible (no associated data)
let checkDivisibility value =
match value with
| DivisibleBy 3 quotient ->
// => Parameterized: DivisibleBy 3 tests divisibility by 3
// => quotient bound to value/3 if divisible
sprintf "%d / 3 = %d" value quotient
| NotDivisibleBy -> // => Not divisible by 3
sprintf "%d not divisible by 3" value
printfn "%s" (checkDivisibility 15)
// => Outputs: 15 / 3 = 5
printfn "%s" (checkDivisibility 7)
// => Outputs: 7 not divisible by 3Key Takeaway: Multi-case patterns use (|Case1|Case2|...). Cases can carry data. All cases must be covered in match.
Why It Matters: Active patterns enable domain-specific matching that extends pattern matching beyond the type system with computed classifications. Compiler lexers classify tokens using active patterns: | Keyword str -> handleKeyword str | Identifier name -> handleId name | IntLiteral n -> handleInt n. This makes complex classification logic exhaustively checkable by the compiler. Domain-driven designs use active patterns to match business rule conditions in declarative, self-documenting match expressions.
Example 43: List Comprehensions Advanced
List comprehensions combine filtering, mapping, and generation in concise syntax.
// Example 43: List Comprehensions Advanced
let squares = [ for x in 1 .. 10 -> x * x ]
// => List comprehension with map
// => for x in range -> expression
// => squares is [1; 4; 9; 16; 25; 36; 49; 64; 81; 100]
printfn "%A" squares // => Outputs: [1; 4; 9; 16; 25; 36; 49; 64; 81; 100]
// Filtering with if:
let evenSquares = [ for x in 1 .. 10 do
if x % 2 = 0 then
yield x * x ]
// => Conditional yield
// => evenSquares is [4; 16; 36; 64; 100]
printfn "%A" evenSquares // => Outputs: [4; 16; 36; 64; 100]
// Multiple generators (cartesian product):
let pairs = [ for x in 1 .. 3 do
for y in 1 .. 3 do
yield (x, y) ]
// => Nested loops create pairs
// => pairs is [(1,1); (1,2); (1,3); (2,1); (2,2); (2,3); (3,1); (3,2); (3,3)]
printfn "%A" pairs // => Outputs: [(1, 1); (1, 2); (1, 3); (2, 1); (2, 2); (2, 3); (3, 1); (3, 2); (3, 3)]
// Complex comprehension:
let pythagoreanTriples = [
for a in 1 .. 20 do
for b in a .. 20 do
for c in b .. 20 do
if a*a + b*b = c*c then
yield (a, b, c) ]
// => Find Pythagorean triples (a²+b²=c²)
// => Nested loops with filtering
// => pythagoreanTriples is [(3,4,5); (5,12,13); (6,8,10); ...]
printfn "%A" pythagoreanTriples
// => Outputs: [(3, 4, 5); (5, 12, 13); (6, 8, 10); (8, 15, 17); (9, 12, 15); (12, 16, 20)]
// Comprehension with function calls:
let lengths = [ for str in ["hello"; "world"; "F#"] -> str.Length ]
// => Map strings to lengths
// => lengths is [5; 5; 2]
printfn "%A" lengths // => Outputs: [5; 5; 2]Key Takeaway: List comprehensions use [ for x in source -> expr ] for mapping, yield for conditional inclusion. Multiple for clauses create nested iterations.
Why It Matters: Comprehensions provide concise, readable syntax for complex list generation and transformation. Data scientists generate hyperparameter combinations for grid search using nested comprehensions, creating thousands of test configurations in 3-4 lines. Cartesian product generation, Pythagorean triple finding, and combinatorial enumeration all become one-liners. The declarative syntax reduces the risk of off-by-one errors common in equivalent nested for-loop implementations.
Example 44: Set and Map Collections
Sets provide efficient membership testing; Maps enable key-value lookups with immutability.
// Example 44: Set and Map Collections
let set1 = Set.ofList [1; 2; 3; 4; 5]
// => Set from list (duplicates removed)
// => Type: Set<int> (immutable, ordered)
let set2 = Set.ofList [4; 5; 6; 7; 8]
// => Second set
let union = Set.union set1 set2
// => Union: all elements from both sets
// => union is {1; 2; 3; 4; 5; 6; 7; 8}
let intersection = Set.intersect set1 set2
// => Intersection: elements in both
// => intersection is {4; 5}
let difference = Set.difference set1 set2
// => Difference: elements in set1 but not set2
// => difference is {1; 2; 3}
printfn "Union: %A" union
// => Outputs: Union: set [1; 2; 3; 4; 5; 6; 7; 8]
printfn "Intersection: %A" intersection
// => Outputs: Intersection: set [4; 5]
printfn "Difference: %A" difference
// => Outputs: Difference: set [1; 2; 3]
let contains = Set.contains 3 set1
// => Membership test (O(log n))
// => contains is true
printfn "Contains 3: %b" contains
// => Outputs: Contains 3: true
// Map collection:
let map = Map.ofList [("Alice", 30); ("Bob", 25); ("Charlie", 35)]
// => Map from key-value pairs
// => Type: Map<string, int> (immutable)
let aliceAge = Map.find "Alice" map
// => Lookup value by key (throws if not found)
// => aliceAge is 30
let bobAge = Map.tryFind "Bob" map
// => Safe lookup returning option
// => bobAge is Some 25
let unknownAge = Map.tryFind "Dave" map
// => Key not found
// => unknownAge is None
let updatedMap = Map.add "Dave" 28 map
// => Add new key-value (creates new map)
// => Original map unchanged
// => updatedMap has 4 entries
let removedMap = Map.remove "Alice" updatedMap
// => Remove key (creates new map)
// => removedMap has 3 entries (Bob, Charlie, Dave)
printfn "Alice's age: %d" aliceAge
// => Outputs: Alice's age: 30
printfn "Bob's age: %A" bobAge
// => Outputs: Bob's age: Some 25
printfn "Unknown age: %A" unknownAge
// => Outputs: Unknown age: None
printfn "Map size: %d" (Map.count removedMap)
// => Outputs: Map size: 3Key Takeaway: Sets provide set operations (union, intersection, difference) with fast membership testing. Maps provide immutable key-value storage.
Why It Matters: Sets eliminate duplicates efficiently for large datasets and enable mathematical set operations used throughout computing. Search engines store unique document IDs as sets, with union/intersection operations implementing boolean search queries (AND/OR/NOT) in O(n log n) time. Immutable sets use structural sharing to reduce memory when creating derived sets. Maps provide fast key-value lookup for caches, indexes, and configuration stores with O(log n) access time.
Example 45: Generic Functions
Generic functions work with any type using type parameters, enabling code reuse without duplication.
// Example 45: Generic Functions
let identity x = x // => Generic function (type inferred)
// => Type: 'a -> 'a (generic type parameter)
// => Works with any type
let intValue = identity 42
// => Type inference: 'a = int
// => intValue is 42
let stringValue = identity "hello"
// => Type inference: 'a = string
// => stringValue is "hello"
printfn "%d" intValue // => Outputs: 42
printfn "%s" stringValue // => Outputs: hello
// Explicit type parameters:
let swap<'a, 'b> (x: 'a, y: 'b) : ('b * 'a) =
// => Generic function with 2 type parameters
// => 'a and 'b can be different types
(y, x) // => Returns swapped tuple
let swapped = swap (1, "two")
// => swapped is ("two", 1)
// => Type: string * int
printfn "%A" swapped // => Outputs: ("two", 1)
// Generic list operations:
let first<'T> (list: 'T list) : 'T option =
// => Generic list function
match list with
| [] -> None // => Empty list: None
| head :: _ -> Some head
// => Non-empty: Some first element
let firstInt = first [1; 2; 3]
// => firstInt is Some 1 (type: int option)
let firstString = first ["a"; "b"; "c"]
// => firstString is Some "a" (type: string option)
let firstEmpty = first []
// => firstEmpty is None (type: 'a option)
printfn "%A" firstInt // => Outputs: Some 1
printfn "%A" firstString // => Outputs: Some "a"
printfn "%A" firstEmpty // => Outputs: None
// Constrained generics (SRTP - Statically Resolved Type Parameters):
let inline add x y = x + y
// => inline enables static resolution
// => Works with any type supporting +
// => Type: ^a -> ^a -> ^a when ^a : (static member (+) : ^a * ^a -> ^a)
let intSum = add 1 2 // => intSum is 3 (int)
let floatSum = add 1.5 2.5
// => floatSum is 4.0 (float)
let stringConcat = add "hello" " world"
// => stringConcat is "hello world" (string)
printfn "%d" intSum // => Outputs: 3
printfn "%.1f" floatSum // => Outputs: 4.0
printfn "%s" stringConcat// => Outputs: hello worldKey Takeaway: Generic functions use type parameters 'a, 'b. Use inline for operator-generic functions requiring static resolution.
Why It Matters: Generics eliminate code duplication across types, enabling algorithms and data structures that work correctly for any type while maintaining full compile-time type safety. F#'s standard library implements all collection operations generically, providing the same List.map, Array.filter, and Seq.fold for every element type. SRTP (statically resolved type parameters) extend generics to operator-constrained functions, enabling type-safe numeric algorithms that work for int, float, and decimal without boxing.
Example 46: Function Recursion Patterns
Recursion patterns include direct recursion, mutual recursion, and continuation-passing style.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
A[factorial 5]:::blue --> B[5 * factorial 4]:::orange
B --> C[5 * 4 * factorial 3]:::orange
C --> D[5 * 4 * 3 * factorial 2]:::orange
D --> E[5 * 4 * 3 * 2 * 1]:::teal
E --> F[Result: 120]:::teal
style A fill:#0173B2,stroke:#000,color:#fff
style B fill:#DE8F05,stroke:#000,color:#fff
style C fill:#DE8F05,stroke:#000,color:#fff
style D fill:#DE8F05,stroke:#000,color:#fff
style E fill:#029E73,stroke:#000,color:#fff
style F fill:#029E73,stroke:#000,color:#fff
Code:
// Example 46: Function Recursion Patterns
let rec factorial n = // => Standard recursion (NOT tail-recursive)
// => rec keyword enables self-reference
if n <= 1 then 1 // => Base case
else n * factorial (n - 1)
// => Recursive case (multiplication AFTER call)
// => Stack frame accumulates: n * (n-1) * ... * 1
let result1 = factorial 5
// => 5 * 4 * 3 * 2 * 1 = 120
// => result1 is 120
printfn "Factorial 5: %d" result1
// => Outputs: Factorial 5: 120
// Mutual recursion:
let rec isEven n = // => rec in first function of mutually recursive pair
if n = 0 then true
elif n = 1 then false
else isOdd (n - 1) // => Calls isOdd (defined next)
and isOdd n = // => and connects mutually recursive functions
if n = 0 then false
elif n = 1 then true
else isEven (n - 1) // => Calls isEven
let evenResult = isEven 10
// => evenResult is true
let oddResult = isOdd 7 // => oddResult is true
printfn "10 is even: %b" evenResult
// => Outputs: 10 is even: true
printfn "7 is odd: %b" oddResult
// => Outputs: 7 is odd: true
// Pattern matching in recursion:
let rec sumList list = // => Recursive list sum
match list with
| [] -> 0 // => Base case: empty list
| head :: tail -> // => Recursive case: decompose list
head + sumList tail
// => Add head to sum of tail
let listSum = sumList [1; 2; 3; 4; 5]
// => 1 + 2 + 3 + 4 + 5 = 15
// => listSum is 15
printfn "List sum: %d" listSum
// => Outputs: List sum: 15Key Takeaway: Use rec for self-referencing functions. Use and for mutually recursive functions. Pattern matching common in recursive list processing.
Why It Matters: Recursion provides natural expression for tree/graph algorithms where iteration would require explicit stack management. Compilers traverse abstract syntax trees recursively using pattern matching - the code structure mirrors the data structure. JSON parsers, HTML processors, and expression evaluators all benefit from recursive descent. Mutual recursion (rec/and) enables state machines and grammar rules that reference each other directly, matching formal grammar specifications.
Example 47: Tail Recursion
Tail recursion enables efficient recursion without stack overflow by optimizing tail calls to loops.
// Example 47: Tail Recursion
let rec factorialTail n acc =
// => Tail-recursive factorial
// => acc is accumulator (carries result)
if n <= 1 then acc // => Base case: return accumulator
else factorialTail (n - 1) (n * acc)
// => Recursive call is LAST operation (tail position)
// => Compiler optimizes to while loop (no stack growth)
let factorial n = factorialTail n 1
// => Wrapper with initial accumulator
let result1 = factorial 5
// => Call sequence:
// => factorialTail 5 1
// => factorialTail 4 5 (5*1)
// => factorialTail 3 20 (4*5)
// => factorialTail 2 60 (3*20)
// => factorialTail 1 120 (2*60)
// => Returns 120
// => result1 is 120
printfn "Factorial 5: %d" result1
// => Outputs: Factorial 5: 120
let result2 = factorial 10000
// => Large input (would stack overflow without tail call)
// => Runs efficiently with constant stack space
printfn "Factorial 10000 computed"
// => Outputs: Factorial 10000 computed
// Tail-recursive list sum:
let rec sumListTail list acc =
// => Tail-recursive sum with accumulator
match list with
| [] -> acc // => Base case: return accumulator
| head :: tail ->
sumListTail tail (acc + head)
// => Recursive call in tail position
// => Accumulator carries running sum
let sumList list = sumListTail list 0
// => Wrapper with initial accumulator
let listSum = sumList [1; 2; 3; 4; 5]
// => Call sequence:
// => sumListTail [1;2;3;4;5] 0
// => sumListTail [2;3;4;5] 1 (0+1)
// => sumListTail [3;4;5] 3 (1+2)
// => sumListTail [4;5] 6 (3+3)
// => sumListTail [5] 10 (6+4)
// => sumListTail [] 15 (10+5)
// => Returns 15
// => listSum is 15
printfn "List sum: %d" listSum
// => Outputs: List sum: 15Key Takeaway: Tail recursion requires recursive call as last operation. Use accumulator parameter to carry state. Compiler optimizes to loops.
Why It Matters: Tail recursion prevents stack overflow for deep recursion by compiling to efficient while loops internally. Parsers processing deeply nested JSON (1000+ levels) or deeply recursive data structures use tail-recursive descent without stack limit concerns. The CLR stack typically supports around 1,000-10,000 frames before overflow; tail call optimization enables effectively unlimited recursion depth. Use accumulator parameters to convert non-tail recursive functions to tail-recursive equivalents.
Example 48: Mutual Recursion
Mutual recursion enables functions calling each other, useful for state machines and alternating patterns.
// Example 48: Mutual Recursion
let rec processEven n = // => Processes even state (rec required for mutual)
if n = 0 then // => Base case: both functions terminate at 0
printfn "Done"
else
printfn "Even: %d" n
// => Print even number
processOdd (n - 1)
// => Transition to odd state (calls sibling function)
and processOdd n = // => Processes odd state (and keyword for mutual recursion)
if n = 0 then // => Base case
printfn "Done"
else
printfn "Odd: %d" n
// => Print odd number
processEven (n - 1)
// => Transition to even state (calls back to processEven)
processEven 5 // => Start with even state (n=5)
// => Outputs:
// => Even: 5
// => Odd: 4
// => Even: 3
// => Odd: 2
// => Even: 1
// => Done
// Mutual recursion for expression evaluation:
type Expr = // => Recursive discriminated union for expressions
| Number of int // => Terminal: literal number
| Add of Expr * Expr // => Non-terminal: left + right
| Multiply of Expr * Expr
// => Non-terminal: left * right
let rec evalExpr expr = // => Evaluate expression (main evaluator)
match expr with
| Number n -> n // => Base case: return number
| Add(left, right) ->
evalTerm left + evalTerm right
// => Addition: evaluate both sides via evalTerm
| Multiply(left, right) ->
evalExpr left * evalExpr right
// => Multiplication: direct recursion to evalExpr
and evalTerm term = // => Evaluate term (for operator precedence)
evalExpr term // => Calls back to evalExpr
// => Demonstrates mutual recursion pattern
let expression = Add(Number 10, Multiply(Number 5, Number 3))
// => 10 + (5 * 3) as expression tree
let result = evalExpr expression
// => Evaluates to 10 + 15 = 25
// => result is 25
printfn "Result: %d" result
// => Outputs: Result: 25Key Takeaway: Use and to define mutually recursive functions. Each function can reference the others.
Why It Matters: Mutual recursion models state machines and formal grammars naturally, where functions represent states or grammar rules that reference each other. Recursive descent parsers define mutually recursive functions mirroring grammar productions: parseStatement calls parseExpression, which calls parsePrimary, which calls parseStatement for nested blocks. This structure directly encodes grammar rules in code, making parsers maintainable when grammar changes and enabling straightforward extension with new syntax.
Example 49: Memoization
Memoization caches function results to avoid redundant computation, trading memory for speed.
// Example 49: Memoization
let memoize f = // => Generic memoization function
let cache = System.Collections.Generic.Dictionary<_, _>()
// => Mutable cache (side effect)
fun x -> // => Returns memoized function
match cache.TryGetValue(x) with
| (true, result) ->
// => Cache hit: return cached result
printfn "Cache hit for %A" x
result
| (false, _) -> // => Cache miss: compute and store
printfn "Computing %A" x
let result = f x
// => Call original function
cache.[x] <- result
// => Store in cache
result
let rec fib n = // => Naive Fibonacci (exponential time)
if n <= 1 then n
else fib (n - 1) + fib (n - 2)
// => Recomputes same values many times
let fibMemo = memoize fib
// => Memoized Fibonacci
let result1 = fibMemo 10 // => First call: computes fib(10)
// => Outputs: Computing 10, Computing 9, ... (many computations)
// => result1 is 55
let result2 = fibMemo 10 // => Second call: cache hit
// => Outputs: Cache hit for 10
// => result2 is 55 (instant)
printfn "Fib(10): %d" result1
// => Outputs: Fib(10): 55
printfn "Fib(10) cached: %d" result2
// => Outputs: Fib(10) cached: 55
// Memoized expensive computation:
let expensiveCompute n = // => Simulates expensive operation
System.Threading.Thread.Sleep(1000)
// => Sleep 1 second
n * n // => Return result
let memoizedCompute = memoize expensiveCompute
let r1 = memoizedCompute 5
// => First call: sleeps 1s, computes
// => Outputs: Computing 5
// => r1 is 25 (after 1s)
let r2 = memoizedCompute 5
// => Second call: instant (cached)
// => Outputs: Cache hit for 5
// => r2 is 25 (instant)
printfn "Result 1: %d" r1
// => Outputs: Result 1: 25
printfn "Result 2: %d" r2
// => Outputs: Result 2: 25Key Takeaway: Memoization caches expensive computations. Use dictionary to map inputs to results. Trade memory for speed.
Why It Matters: Memoization dramatically improves performance for recursive algorithms with overlapping subproblems. Dynamic programming solutions for knapsack, edit distance, and sequence alignment use memoization to reduce exponential time complexity to polynomial. The generic memoize function works with any pure function, making it reusable across algorithm implementations. Cache-aside patterns in production systems memoize expensive database lookups or computation-heavy operations with configurable TTL.
Example 50: Module Organization
Modules organize related functions and types, providing namespacing and encapsulation.
// Example 50: Module Organization
module MathUtils = // => Module declaration
// => Groups related functionality
let add x y = x + y // => Public function (default)
let multiply x y = x * y
let private helper x =
// => Private function (module-scoped)
// => Not accessible outside module
x * 2
let double x = helper x
// => Public function using private helper
module StringUtils = // => Separate module
let toUpper (s: string) = s.ToUpper()
let toLower (s: string) = s.ToLower()
let concat (strings: string list) =
System.String.Join(", ", strings)
// Using modules:
let sum = MathUtils.add 10 20
// => Qualified access: Module.function
// => sum is 30
let product = MathUtils.multiply 5 6
// => product is 30
let doubled = MathUtils.double 7
// => Uses private helper internally
// => doubled is 14
// let invalid = MathUtils.helper 10
// => COMPILE ERROR: helper is private
let upper = StringUtils.toUpper "hello"
// => upper is "HELLO"
let combined = StringUtils.concat ["F#"; "is"; "great"]
// => combined is "F#, is, great"
printfn "Sum: %d" sum // => Outputs: Sum: 30
printfn "Upper: %s" upper// => Outputs: Upper: HELLO
// Opening modules for unqualified access:
open MathUtils // => Opens module for unqualified access
let sum2 = add 15 25 // => No MathUtils. prefix needed
// => sum2 is 40
printfn "Sum2: %d" sum2 // => Outputs: Sum2: 40
// Nested modules:
module Geometry = // => Parent module
module Circle = // => Nested module
let area radius = System.Math.PI * radius * radius
let circumference radius = 2.0 * System.Math.PI * radius
module Rectangle = // => Another nested module
let area width height = width * height
let perimeter width height = 2.0 * (width + height)
let circleArea = Geometry.Circle.area 5.0
// => Nested module access
// => circleArea is ~78.54
let rectPerimeter = Geometry.Rectangle.perimeter 4.0 6.0
// => rectPerimeter is 20.0
printfn "Circle area: %.2f" circleArea
// => Outputs: Circle area: 78.54
printfn "Rectangle perimeter: %.2f" rectPerimeter
// => Outputs: Rectangle perimeter: 20.00Key Takeaway: Use module Name to organize code. Functions are public by default, use private for internal helpers. Access with Module.function or open Module.
Why It Matters: Modules provide namespacing and encapsulation without the overhead of class definitions, reducing boilerplate in functional codebases. Large F# projects organize thousands of functions into hierarchical modules like Domain.Pricing.Calculate and Infrastructure.Database.Queries with clear boundaries. The private keyword restricts visibility to module scope, enforcing information hiding. open declarations at file scope reduce verbosity while maintaining namespace clarity.
Example 51: Namespaces
Namespaces group modules at higher level, similar to C# namespaces, for large-scale organization.
// Example 51: Namespaces
namespace MyCompany.Utils
// => Namespace declaration at file top
// => Applies to all modules in this file
module StringHelpers = // => Module within namespace (fully qualified: MyCompany.Utils.StringHelpers)
let reverse (s: string) =
// => Reverse string characters
s.ToCharArray() |> Array.rev |> System.String
// => ToCharArray -> reverse array -> create string
let capitalize (s: string) =
if s.Length = 0 then s
// => Empty string: return unchanged
else s.[0..0].ToUpper() + s.[1..]
// => Uppercase first char + rest unchanged
module MathHelpers = // => Another module in same namespace
let square x = x * x // => x squared
let cube x = x * x * x
// => x cubed
// Using namespaced modules:
open MyCompany.Utils // => Open namespace (enables module-qualified access)
let reversed = StringHelpers.reverse "hello"
// => reversed is "olleh"
let capitalized = StringHelpers.capitalize "world"
// => capitalized is "World"
let squared = MathHelpers.square 7
// => squared is 49
printfn "Reversed: %s" reversed
// => Outputs: Reversed: olleh
printfn "Capitalized: %s" capitalized
// => Outputs: Capitalized: World
printfn "Squared: %d" squared
// => Outputs: Squared: 49
// Alternative: open specific module:
open MyCompany.Utils.StringHelpers
// => Opens specific module (enables unqualified access)
let rev2 = reverse "F#" // => No StringHelpers. prefix needed
// => rev2 is "#F"
printfn "Reversed F#: %s" rev2
// => Outputs: Reversed F#: #FKey Takeaway: Use namespace Company.Product.Component for top-level organization. Namespaces contain modules, modules contain functions.
Why It Matters: Namespaces prevent naming conflicts in large multi-team codebases and .NET assemblies. Enterprise applications use hierarchical namespaces like Contoso.Trading.Execution and Contoso.Trading.Reporting to isolate components, enabling parallel development without function name collisions. Namespaces also improve discoverability through IDE tools - Ctrl+. in Visual Studio navigates namespace hierarchies efficiently. F# namespaces map directly to .NET namespaces, ensuring interop with C# consumers.
Example 52: Signature Files (.fsi)
Signature files define public interfaces, hiding implementation details and enabling encapsulation.
// Example 52: Signature Files (.fsi)
// File: Calculator.fsi (signature/interface file)
(*
module Calculator
val add : int -> int -> int
// => Public function signature
// => Only declared functions are public
val multiply : int -> int -> int
// => Another public function
// Note: subtract is NOT declared in signature
// Therefore it's PRIVATE to implementation
*)
// File: Calculator.fs (implementation file)
module Calculator
let add x y = x + y // => Public (in signature)
let multiply x y = x * y // => Public (in signature)
let subtract x y = x - y // => PRIVATE (not in signature)
// => Only accessible within module
let addThenMultiply x y z =
// => Public function using private helper
let sum = add x y // => Can use both public and private functions
let diff = subtract sum 10
// => Uses private subtract
multiply diff z
// Usage (in another file):
// open Calculator
// let r1 = add 10 20 // => OK: add is public
// let r2 = multiply 5 6 // => OK: multiply is public
// let r3 = subtract 10 5// => ERROR: subtract not in signature (private)
printfn "Module defined (signature example)"
// => Outputs: Module defined (signature example)
// Demonstration (within same file for example):
let publicResult = Calculator.add 15 25
// => publicResult is 40
let composedResult = Calculator.addThenMultiply 10 20 3
// => (10+20-10) * 3 = 60
// => composedResult is 60
printfn "Public result: %d" publicResult
// => Outputs: Public result: 40
printfn "Composed result: %d" composedResult
// => Outputs: Composed result: 60Key Takeaway: Signature files (.fsi) declare public API. Implementation files (.fs) define all functions. Only functions in signature are public.
Why It Matters: Signature files enable information hiding critical for stable library design - the most important software engineering principle for long-term maintainability. Public APIs expose minimal surface area while implementations evolve freely using private helpers. When signature files define the contract, internal refactoring cannot accidentally break callers. .fsi files also serve as authoritative API documentation, showing exactly what's public with precise type signatures.
Example 53: Object Programming - Classes
F# supports OOP for .NET interop and when mutation is necessary, though functional style preferred.
// Example 53: Object Programming - Classes
type Person(name: string, age: int) =
// => Class definition with primary constructor
// => Constructor parameters: name, age
let mutable currentAge = age
// => Private mutable field
// => let bindings in class are private
member this.Name = name
// => Public property (getter only)
// => this is self-reference
member this.Age = currentAge
// => Property exposing private field
member this.HaveBirthday() =
// => Method with side effect (unit return)
currentAge <- currentAge + 1
// => Mutate private state
printfn "%s is now %d" name currentAge
member this.Greet() =// => Method returning string
sprintf "Hello, I'm %s, aged %d" name currentAge
// Using class:
let alice = Person("Alice", 30)
// => Create instance using constructor
printfn "%s" (alice.Greet())
// => Outputs: Hello, I'm Alice, aged 30
alice.HaveBirthday() // => Call method (side effect)
// => Outputs: Alice is now 31
printfn "Age after birthday: %d" alice.Age
// => Outputs: Age after birthday: 31
// Class with additional constructors:
type Rectangle(width: float, height: float) =
member this.Width = width
member this.Height = height
member this.Area = width * height
// => Computed property
new(side: float) = // => Additional constructor (square)
Rectangle(side, side)
// => Delegates to primary constructor
let rect = Rectangle(4.0, 6.0)
// => Uses primary constructor
let square = Rectangle(5.0)
// => Uses additional constructor (square)
printfn "Rectangle area: %.1f" rect.Area
// => Outputs: Rectangle area: 24.0
printfn "Square area: %.1f" square.Area
// => Outputs: Square area: 25.0Key Takeaway: Classes use type Name(params) = with members. Use mutable for mutable state. Prefer functional style; use classes for .NET interop.
Why It Matters: Classes enable F# seamless integration with OOP-heavy .NET frameworks and existing C# codebases. F# code implements ASP.NET Core controllers, Entity Framework models, and WPF ViewModels using class syntax while keeping domain and business logic in functional style. Teams adopt F# incrementally by adding F# class libraries to C# projects, leveraging functional correctness for algorithms while maintaining OOP-compatible interfaces for framework integration.
Example 54: Object Programming - Interfaces
Interfaces define contracts for classes, enabling polymorphism and dependency injection.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
A[IShape interface]:::blue --> B[Circle implements]:::orange
A --> C[Rectangle implements]:::teal
B --> D[Area method]:::orange
C --> E[Area method]:::teal
style A fill:#0173B2,stroke:#000,color:#fff
style B fill:#DE8F05,stroke:#000,color:#fff
style C fill:#029E73,stroke:#000,color:#fff
style D fill:#DE8F05,stroke:#000,color:#fff
style E fill:#029E73,stroke:#000,color:#fff
Code:
// Example 54: Object Programming - Interfaces
type IShape = // => Interface definition (contract, no implementation)
abstract member Area : unit -> float
// => Abstract method signature: unit -> float
abstract member Perimeter : unit -> float
// => Another abstract method signature
type Circle(radius: float) =
// => Class implementing IShape (radius is constructor param)
interface IShape with // => Explicit interface implementation block
member this.Area() =
System.Math.PI * radius * radius
// => Area = π * r² (captures radius from constructor)
member this.Perimeter() =
2.0 * System.Math.PI * radius
// => Perimeter = 2πr
type Rectangle(width: float, height: float) =
// => Another IShape implementation
interface IShape with
member this.Area() =
width * height // => Area = width * height
member this.Perimeter() =
2.0 * (width + height)
// => Perimeter = 2 * (w + h)
// Using interfaces:
let circle = Circle(5.0) :> IShape
// => Create Circle then upcast to IShape with :>
// => circle type is now IShape (not Circle)
let rect = Rectangle(4.0, 6.0) :> IShape
// => rect type is IShape (not Rectangle)
let shapes = [ circle; rect ]
// => Polymorphic list: both elements are IShape
for shape in shapes do // => Iterate over IShape list (polymorphic dispatch)
printfn "Area: %.2f, Perimeter: %.2f"
(shape.Area()) // => Virtual dispatch: calls Circle.Area or Rectangle.Area
(shape.Perimeter())
// => Outputs:
// => Area: 78.54, Perimeter: 31.42 (circle)
// => Area: 24.00, Perimeter: 20.00 (rectangle)
// Interface with properties:
type IPerson = // => Interface with property signatures
abstract member Name : string
// => Property (read-only, no unit parameter)
abstract member Age : int
// => Another property
type Employee(name: string, age: int, salary: float) =
// => Implements IPerson plus has extra Salary member
interface IPerson with
member this.Name = name // => Property implementation
member this.Age = age // => Property implementation
member this.Salary = salary
// => Additional member NOT in IPerson interface
let emp = Employee("Bob", 35, 75000.0)
// => Concrete Employee instance
let person = emp :> IPerson
// => Upcast to IPerson (loses Salary visibility)
printfn "Person: %s, %d" person.Name person.Age
// => Outputs: Person: Bob, 35
// printfn "Salary: %.0f" person.Salary
// => COMPILE ERROR: Salary not in IPerson interfaceKey Takeaway: Interfaces use abstract member. Classes implement with interface IName with. Upcast to interface with :>.
Why It Matters: Interfaces enable dependency injection and testability by abstracting dependencies behind contracts. Application layers depend on IRepository or IEmailService interfaces instead of concrete implementations, allowing test doubles to replace real databases or external services during unit testing. F# object expressions (Example 55) create lightweight interface implementations without class boilerplate, making DI in F# more concise than equivalent C# patterns.
Example 55: Object Expressions
Object expressions create interface implementations inline without defining named classes.
// Example 55: Object Expressions
type ILogger = // => Interface for logging
abstract member Log : string -> unit
// => Log takes string, returns unit (void)
// Object expression implementing interface:
let consoleLogger = // => Create ILogger implementation inline
{ new ILogger with // => Object expression syntax: { new IType with ... }
member this.Log(message) =
// => Implement interface member inline
printfn "[LOG] %s" message }
// => Body: print message with [LOG] prefix
// => consoleLogger type: ILogger
consoleLogger.Log("Application started")
// => Outputs: [LOG] Application started
// Object expression with state:
let createCounter() = // => Factory returning interface implementation
let mutable count = 0// => Captured mutable state (closure variable)
{ new System.IDisposable with
// => Implement standard IDisposable interface
member this.Dispose() =
// => Dispose member: called when resource released
count <- count + 1
// => Increment captured counter (mutable state)
printfn "Disposed %d times" count }
// => Returns IDisposable with captured state
let counter1 = createCounter()
// => counter1 type: IDisposable
counter1.Dispose() // => Outputs: Disposed 1 times
counter1.Dispose() // => Outputs: Disposed 2 times
let counter2 = createCounter()
// => Separate instance with own count (new closure)
counter2.Dispose() // => Outputs: Disposed 1 times
// Passing object expression to function:
let useLogger (logger: ILogger) message =
// => Accepts any ILogger implementation
logger.Log(message) // => Dispatch through interface (polymorphic call)
useLogger consoleLogger "Processing data"
// => Outputs: [LOG] Processing data
useLogger
{ new ILogger with
// => Inline object expression as function argument
member this.Log(msg) =
// => Different implementation: ERROR prefix
printfn "[ERROR] %s" msg }
"Something failed" // => Inline object expression as argument
// => Outputs: [ERROR] Something failedKey Takeaway: Object expressions use { new IInterface with members } to create inline implementations without named classes.
Why It Matters: Object expressions enable lightweight, inline interface implementations without requiring named class definitions, reducing ceremony for single-use adapters and test doubles. GUI frameworks receive event listeners as object expressions without polluting namespaces with single-use classes. Unit tests create mock dependencies inline: { new IRepository with member _.GetUser id = Some testUser } in a single line. This pattern is particularly valuable for testing code that depends on interfaces.
Example 56: Discriminated Unions Advanced - Recursive Types
Recursive discriminated unions model tree and list structures naturally.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC
graph TD
A[BinaryTree]:::blue --> B[Empty]:::orange
A --> C[Node of value * left * right]:::teal
C --> D[Left subtree]:::purple
C --> E[Right subtree]:::purple
style A fill:#0173B2,stroke:#000,color:#fff
style B fill:#DE8F05,stroke:#000,color:#fff
style C fill:#029E73,stroke:#000,color:#fff
style D fill:#CC78BC,stroke:#000,color:#fff
style E fill:#CC78BC,stroke:#000,color:#fff
Code:
// Example 56: Discriminated Unions Advanced - Recursive Types
type BinaryTree<'T> = // => Generic binary tree
| Empty // => Leaf case: no value
| Node of value: 'T * left: BinaryTree<'T> * right: BinaryTree<'T>
// => Branch case: value + two subtrees
// => RECURSIVE: contains BinaryTree
// Building tree:
let tree =
Node(10, // => Root value: 10
Node(5, // => Left subtree root: 5
Node(3, Empty, Empty),
// => Left-left: 3 (leaf)
Node(7, Empty, Empty)),
// => Left-right: 7 (leaf)
Node(15, // => Right subtree root: 15
Empty, // => Right-left: empty
Node(20, Empty, Empty)))
// => Right-right: 20 (leaf)
// Tree traversal (inorder):
let rec inorder tree = // => Recursive tree traversal
match tree with
| Empty -> [] // => Base case: empty tree
| Node(value, left, right) ->
(inorder left) @ [value] @ (inorder right)
// => Inorder: left, value, right
// => @ is list concatenation
let sorted = inorder tree
// => sorted is [3; 5; 7; 10; 15; 20]
// => Inorder traversal of BST yields sorted sequence
printfn "Inorder: %A" sorted
// => Outputs: Inorder: [3; 5; 7; 10; 15; 20]
// Tree operations:
let rec contains value tree =
// => Search tree for value
match tree with
| Empty -> false // => Not found
| Node(v, left, right) ->
if value = v then true
// => Found
elif value < v then contains value left
// => Search left subtree
else contains value right
// => Search right subtree
let has7 = contains 7 tree
// => has7 is true
let has99 = contains 99 tree
// => has99 is false
printfn "Contains 7: %b" has7
// => Outputs: Contains 7: true
printfn "Contains 99: %b" has99
// => Outputs: Contains 99: false
// Custom list type (demonstrating recursion):
type MyList<'T> = // => Custom list implementation
| Nil // => Empty list
| Cons of head: 'T * tail: MyList<'T>
// => Cons cell (like ::)
let myList = Cons(1, Cons(2, Cons(3, Nil)))
// => [1; 2; 3] in custom list
let rec myLength list = // => Length function
match list with
| Nil -> 0 // => Empty: length 0
| Cons(_, tail) -> // => Cons: 1 + length of tail
1 + myLength tail
let len = myLength myList
// => len is 3
printfn "MyList length: %d" len
// => Outputs: MyList length: 3Key Takeaway: Recursive DUs reference themselves in case definitions. Perfect for tree/list structures. Pattern matching decomposes recursively.
Why It Matters: Recursive discriminated unions naturally model hierarchical data structures like trees, grammars, and nested documents. Compilers represent abstract syntax trees as recursive DUs, with pattern matching traversing them naturally - the code reads like grammar rules. JSON parsers model data as type Json = JNull | JBool of bool | JNumber of float | JString of string | JArray of Json list | JObject of (string * Json) list, enabling recursive processing with exhaustive case coverage.
Example 57: Record Update Syntax - with Keyword
Records support functional updates creating new records with modified fields while sharing unchanged fields.
// Example 57: Record Update Syntax - with Keyword
type Person = { // => Record type with four fields
FirstName: string // => String field
LastName: string // => String field
Age: int // => Integer field
City: string // => String field
}
let alice = { // => Create initial record
FirstName = "Alice" // => Assign string field
LastName = "Smith" // => Assign string field
Age = 30 // => Assign int field
City = "Jakarta" // => Assign string field
} // => alice type: Person (immutable record)
printfn "%A" alice // => Outputs: { FirstName = "Alice"; LastName = "Smith"; Age = 30; City = "Jakarta" }
// Update single field:
let aliceOlder = { alice with Age = 31 }
// => { record with Field = newValue } syntax
// => Copy alice, change Age to 31
// => FirstName, LastName, City unchanged
// => Original alice unmodified (immutable)
printfn "%A" aliceOlder // => Outputs: { FirstName = "Alice"; LastName = "Smith"; Age = 31; City = "Jakarta" }
// Update multiple fields:
let aliceMoved = { alice with Age = 32; City = "Bandung" }
// => Multiple fields separated by semicolon
// => Copy alice, change Age and City
// => FirstName, LastName unchanged
printfn "%A" aliceMoved // => Outputs: { FirstName = "Alice"; LastName = "Smith"; Age = 32; City = "Bandung" }
// Chaining updates:
let aliceRenamed = { alice with FirstName = "Alicia" }
// => Change first name; new record created
let aliceRenamedMoved = { aliceRenamed with City = "Surabaya" }
// => Chain updates (immutable style): base is aliceRenamed
printfn "%A" aliceRenamedMoved
// => Outputs: { FirstName = "Alicia"; LastName = "Smith"; Age = 30; City = "Surabaya" }
// Original unchanged:
printfn "Original alice: %A" alice
// => Outputs: Original alice: { FirstName = "Alice"; LastName = "Smith"; Age = 30; City = "Jakarta" }
// => All updates created NEW records; alice unchanged
// Functional state management:
let updateAge person newAge =
// => Generic update function (works on any Person)
{ person with Age = newAge }
// => Returns new Person with updated Age field
let aliceAt35 = updateAge alice 35
// => aliceAt35 is new record with Age=35
printfn "Alice at 35: %A" aliceAt35
// => Outputs: Alice at 35: { FirstName = "Alice"; LastName = "Smith"; Age = 35; City = "Jakarta" }Key Takeaway: Use { record with Field = value } to create updated copies. Original record unchanged. Multiple fields updatable.
Why It Matters: Record update syntax enables functional state management where each state transition produces a new immutable state, preserving history without additional infrastructure. Redux-style state stores use record updates to produce new application states, naturally supporting time-travel debugging and undo/redo since previous states are never mutated. Event sourcing systems apply event records to state records using { state with ... } syntax, making each transition explicit and reversible.
Example 58: Pattern Matching Guards
Guards add conditional logic to pattern matching, enabling fine-grained case discrimination.
// Example 58: Pattern Matching Guards
let categorizeNumber n = // => Function classifying numbers into ranges
match n with
| 0 -> "zero" // => Literal pattern: exactly 0
| n when n < 0 -> // => Guard: when condition
"negative" // => n is bound, accessible in guard expression
| n when n < 10 -> // => Another guard: 1-9
"small positive" // => n bound to matched value (e.g., 7)
| n when n < 100 -> // => Guard: 10-99
"medium positive"// => n is bound to value in range
| _ -> // => Wildcard: all remaining (>=100)
"large positive"
printfn "%s" (categorizeNumber 0)
// => Outputs: zero
printfn "%s" (categorizeNumber -5)
// => Outputs: negative
printfn "%s" (categorizeNumber 7)
// => Outputs: small positive
printfn "%s" (categorizeNumber 50)
// => Outputs: medium positive
printfn "%s" (categorizeNumber 200)
// => Outputs: large positive
// Guards with deconstruction:
let analyzeList list = // => Guards can combine with list deconstruction
match list with
| [] -> "empty" // => Empty list pattern
| [x] when x > 0 -> // => Single-element list with guard: positive
"single positive"
| [x] when x < 0 -> // => Single element: negative
"single negative"
| [x] -> // => Single element (any value, x=0 here)
"single zero"
| head :: tail when head = 0 ->
// => Deconstruct AND guard: starts with 0
"starts with zero"
| head :: tail when List.length tail > 5 ->
// => Guard calling function: long list (>6 total)
"long list"
| _ -> // => Catch-all remaining cases
"other list"
printfn "%s" (analyzeList [])
// => Outputs: empty
printfn "%s" (analyzeList [5])
// => Outputs: single positive
printfn "%s" (analyzeList [-3])
// => Outputs: single negative
printfn "%s" (analyzeList [0; 1; 2])
// => Outputs: starts with zero
printfn "%s" (analyzeList [1; 2; 3; 4; 5; 6; 7])
// => Outputs: long list
// Complex guards:
let analyzePair (x, y) = // => Guards on tuple deconstruction
match (x, y) with
| (a, b) when a = b ->
// => Both equal: a and b bound from tuple
"equal" // => Returns "equal" for (5,5), (0,0), etc.
| (a, b) when a + b = 10 ->
// => Sum guard: a+b must equal 10
"sum to 10" // => Matches (3,7), (8,2), etc.
| (a, b) when a > b ->
// => First larger: a > b
"first larger" // => Matches (8,3), (10,2), etc.
| _ -> // => Remaining: second >= first
"second larger or equal"
printfn "%s" (analyzePair (5, 5))
// => Outputs: equal
printfn "%s" (analyzePair (3, 7))
// => Outputs: sum to 10
printfn "%s" (analyzePair (8, 3))
// => Outputs: first largerKey Takeaway: Use when clause after pattern for conditional matching. Guards can reference bound values and call functions.
Why It Matters: Pattern matching guards enable complex conditional matching that reads like natural language specifications without deeply nested if/else chains. Business rule engines express discount policies declaratively: | Order amount when amount > 10000 -> ApplyBulkDiscount | Order amount when amount > 1000 -> ApplyTierDiscount. Guards combine with deconstruction patterns to express complex conditions on extracted values, making rule logic self-documenting and exhaustively checked by the compiler.
Example 59: Function Patterns - Pattern Matching in Parameters
Functions can pattern match directly in parameter position, eliminating explicit match expressions.
// Example 59: Function Patterns - Pattern Matching in Parameters
let isEmptyList = function
// => function keyword enables pattern matching
// => Equivalent to: fun list -> match list with
| [] -> true // => Empty list case: returns true
| _ -> false // => Non-empty case: returns false
printfn "[] empty: %b" (isEmptyList [])
// => Outputs: [] empty: true
printfn "[1] empty: %b" (isEmptyList [1])
// => Outputs: [1] empty: false
// Multiple cases:
let describeList = function
// => function with multiple patterns
| [] -> "empty" // => Empty list: returns "empty"
| [x] -> sprintf "single: %d" x
// => Single element list: binds x, returns "single: x"
| [x; y] -> sprintf "pair: %d, %d" x y
// => Two-element list: binds x and y
| head :: tail -> sprintf "list starting with %d" head
// => Head :: tail pattern: binds head and tail
printfn "%s" (describeList [])
// => Outputs: empty
printfn "%s" (describeList [42])
// => Outputs: single: 42
printfn "%s" (describeList [1; 2])
// => Outputs: pair: 1, 2
printfn "%s" (describeList [1; 2; 3; 4])
// => Outputs: list starting with 1
// Pattern matching with tuples:
let addPair = function // => Pattern match on tuple argument
| (x, y) -> x + y // => Deconstruct tuple in pattern; returns sum
let sum = addPair (10, 20)
// => sum is 30
printfn "Sum: %d" sum // => Outputs: Sum: 30
// Multiple parameters with pattern matching:
let rec listSum list = // => Recursive list sum using match expression
match list with
| [] -> 0 // => Base case: empty list sums to 0
| head :: tail -> head + listSum tail
// => Recursive case: head + sum of tail
// Equivalent using function keyword:
let rec listSum2 = function
// => Same logic as listSum but with function keyword
| [] -> 0 // => Base case
| head :: tail -> head + listSum2 tail
// => Recursive: identical behavior to listSum
let total1 = listSum [1; 2; 3; 4; 5]
// => total1 is 15
let total2 = listSum2 [1; 2; 3; 4; 5]
// => total2 is 15 (same result, different syntax)
printfn "Total1: %d" total1
// => Outputs: Total1: 15
printfn "Total2: %d" total2
// => Outputs: Total2: 15
// Option pattern:
let describeOption = function
// => Pattern match on option type
| Some value -> sprintf "Value: %d" value
// => Some case: unwraps value and formats
| None -> "No value" // => None case: returns placeholder string
printfn "%s" (describeOption (Some 42))
// => Outputs: Value: 42
printfn "%s" (describeOption None)
// => Outputs: No valueKey Takeaway: Use function keyword for single-parameter pattern matching. Eliminates explicit match expression. Concise for simple matches.
Why It Matters: The function keyword reduces boilerplate for single-argument pattern matching functions, making list processing and option handling more concise. Consistent use of function | [] -> ... | head :: tail -> ... across a codebase creates recognizable patterns that experienced F# developers read fluently. The pattern is particularly valued for short utility functions where the explicit match x with form adds ceremony without clarity. Functions with function compose naturally with |> pipelines.
Example 60: Collection Functions - choose, collect, partition, groupBy
Advanced collection functions enable complex transformations beyond map/filter/fold.
// Example 60: Collection Functions - choose, collect, partition, groupBy
let numbers = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]
// choose: map + filter combined
let evenSquares = numbers
|> List.choose (fun x ->
if x % 2 = 0 then Some (x * x)
// => Keep even, square it
else None) // => Discard odd
// => choose applies function returning option
// => Keeps only Some values, unwraps them
// => evenSquares is [4; 16; 36; 64; 100]
printfn "Even squares: %A" evenSquares
// => Outputs: Even squares: [4; 16; 36; 64; 100]
// collect: flatMap (map + flatten)
let pairs = [1; 2; 3]
|> List.collect (fun x ->
[x; x * 10])
// => Each element produces list
// => collect flattens results
// => pairs is [1; 10; 2; 20; 3; 30]
printfn "Pairs: %A" pairs
// => Outputs: Pairs: [1; 10; 2; 20; 3; 30]
let nestedFlattened = [[1; 2]; [3; 4]; [5; 6]]
|> List.collect id
// => id is identity function (returns input)
// => Flattens nested lists
// => nestedFlattened is [1; 2; 3; 4; 5; 6]
printfn "Flattened: %A" nestedFlattened
// => Outputs: Flattened: [1; 2; 3; 4; 5; 6]
// partition: split into two lists based on predicate
let evens, odds = numbers |> List.partition (fun x -> x % 2 = 0)
// => Returns tuple (true list, false list)
// => evens is [2; 4; 6; 8; 10]
// => odds is [1; 3; 5; 7; 9]
printfn "Evens: %A" evens
// => Outputs: Evens: [2; 4; 6; 8; 10]
printfn "Odds: %A" odds // => Outputs: Odds: [1; 3; 5; 7; 9]
// groupBy: group elements by key
let grouped = numbers
|> List.groupBy (fun x -> x % 3)
// => Groups by remainder when divided by 3
// => Returns (key * element list) list
// => grouped is [(0, [3;6;9]); (1, [1;4;7;10]); (2, [2;5;8])]
for (key, group) in grouped do
printfn "Mod 3 = %d: %A" key group
// => Outputs:
// => Mod 3 = 1: [1; 4; 7; 10]
// => Mod 3 = 2: [2; 5; 8]
// => Mod 3 = 0: [3; 6; 9]
// Combining operations:
let result = numbers
|> List.filter (fun x -> x > 3)
// => Keep > 3: [4; 5; 6; 7; 8; 9; 10]
|> List.partition (fun x -> x % 2 = 0)
// => Split: ([4;6;8;10], [5;7;9])
|> fun (evens, odds) ->
(List.sum evens, List.sum odds)
// => Sum each partition
// => result is (28, 21)
printfn "Sums (evens, odds): %A" result
// => Outputs: Sums (evens, odds): (28, 21)
// Real-world example: processing transaction data
type Transaction = { Id: int; Amount: float; Category: string }
let transactions = [
{ Id = 1; Amount = 100.0; Category = "Food" }
{ Id = 2; Amount = 50.0; Category = "Transport" }
{ Id = 3; Amount = 200.0; Category = "Food" }
{ Id = 4; Amount = 75.0; Category = "Entertainment" }
{ Id = 5; Amount = 150.0; Category = "Food" }
]
let totalsByCategory = transactions
|> List.groupBy (fun t -> t.Category)
// => Group by category
|> List.map (fun (category, txns) ->
(category, txns |> List.sumBy (fun t -> t.Amount)))
// => Sum amounts per category
|> Map.ofList
// => Convert to map for lookup
for kvp in totalsByCategory do
printfn "%s: %.2f" kvp.Key kvp.Value
// => Outputs:
// => Food: 450.00
// => Transport: 50.00
// => Entertainment: 75.00Key Takeaway: choose combines map+filter, collect flattens mapped results, partition splits by predicate, groupBy groups by key function.
Why It Matters: Advanced collection functions eliminate manual loop and accumulator patterns, expressing complex data transformations declaratively. Financial reporting pipelines group transactions by category, partition by date range, flatten nested collections with collect, and filter with choose in a single pipeline expression. This replaces 50+ lines of imperative accumulation code with 3-4 readable pipeline stages. The resulting code is easier to modify when reporting requirements change.
Next Steps
Continue to Advanced (Examples 61-90+) to learn:
- Advanced async patterns (cancellation tokens, parallel workflows)
- Metaprogramming with quotations
- Computation expression builders (custom workflows)
- Type-level programming
- Advanced interop (P/Invoke, COM)
- Performance optimization patterns
- Concurrency primitives (agents, MailboxProcessor)
- LINQ expression integration
- Advanced type providers
These 30 intermediate examples (Examples 31-60) cover 40-75% of F#'s features, building on beginner fundamentals with production-ready patterns. The remaining 25-60% (advanced) covers specialized features for expert scenarios.
Last updated February 1, 2026