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. Async workflows can handle thousands of concurrent connections on a single thread pool, improving server efficiency compared to thread-per-connection models.
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
do! Async.Sleep 500 // => Simulates 500ms I/O
return "Task 1" // => Returns result
}
let task2 = async { // => Second async task
do! Async.Sleep 500 // => 500ms I/O
return "Task 2"
}
let task3 = async { // => Third async task
do! Async.Sleep 500
return "Task 3"
}
let parallelExecution = async {
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
// => allResults is array of results
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 I/O-bound performance. Web scrapers fetch multiple pages concurrently, improving performance significantly while respecting server rate limits through semaphore controls.
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 async-to-task conversion overhead, improving performance when calling .NET libraries. ASP.NET Core applications benefit from direct task composition without allocation costs of Async.Start/AwaitTask conversions.
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 for Option/Result handling. Railway-oriented programming uses builders to chain operations that may fail, reducing error-handling boilerplate compared to imperative try/catch chains.
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 sequences. Log parsers yield filtered lines on demand, processing gigabyte files with constant memory usage by evaluating one line at a time.
Example 36: Option Computation - Railway-Oriented Programming
Option-based computation expressions enable safe chaining of operations that might fail without explicit null checks.
// Example 36: Option Computation - Railway-Oriented Programming
let tryParseInt (s: string) =
// => Parse string to int option
match System.Int32.TryParse(s) with
| (true, value) -> Some value
// => Success: Some int
| (false, _) -> None // => Failure: None
let tryDivide x y = // => Safe division
if y = 0 then None
else Some (x / y)
type OptionBuilder() = // => Builder for option chaining
member _.Bind(x, f) =
match x with
| Some v -> f v
| None -> None
member _.Return(x) = Some x
let option = OptionBuilder()
let calculate input1 input2 input3 = option {
// => Chain of operations that might fail
let! num1 = tryParseInt input1
// => Parse first input
// => Fails if not valid int
let! num2 = tryParseInt input2
// => Parse second input
let! num3 = tryParseInt input3
// => Parse third input
let! sum = Some (num1 + num2)
// => Sum (trivial Some wrap)
let! result = tryDivide sum num3
// => Divide sum by third number
// => Fails if num3 is 0
return result // => Final result wrapped in Some
}
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
// => parseFailure is None (short-circuit)
let divZeroFailure = calculate "10" "20" "0"
// => Division by zero
// => divZeroFailure is None
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. Payment processing pipelines validate card, check balance, authorize transaction, and debit account using option chains, failing fast at first error without nested if statements.
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+)
| Ok of 'T // => Success case carrying value
| Error of 'TError // => Failure case carrying error
let tryParseInt (s: string) : Result<int, string> =
// => Returns Ok int or Error string
match System.Int32.TryParse(s) with
| (true, value) -> Ok value
// => Success: Ok with parsed value
| (false, _) -> Error (sprintf "Cannot parse '%s' as int" s)
// => Failure: Error with descriptive message
let tryDivide x y : Result<int, string> =
if y = 0 then Error "Division by zero"
// => Error case with reason
else Ok (x / y) // => Success case with result
type ResultBuilder() = // => Builder for result chaining
member _.Bind(x, f) =
match x with
| Ok v -> f v // => Continue with success value
| Error e -> Error e
// => Propagate error
member _.Return(x) = Ok x
let result = ResultBuilder()
let calculate input1 input2 = result {
let! num1 = tryParseInt input1
// => Parse first input
let! num2 = tryParseInt input2
// => Parse second input
let! quotient = tryDivide num1 num2
// => Divide num1 by num2
return quotient // => Return Ok result
}
let success = calculate "20" "4"
// => success is Ok 5 (20/4)
let parseError = calculate "abc" "4"
// => parseError is Error "Cannot parse 'abc' as int"
let divZeroError = calculate "20" "0"
// => 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. Microservices return Result<Data, ApiError> with specific error codes (ValidationFailed, Unauthorized, NotFound), enabling clients to handle errors programmatically without parsing exception messages.
Example 38: Type Providers - JSON
Type providers generate types from external schemas at compile time, providing IntelliSense for JSON structures.
// Example 38: Type Providers - JSON
#r "nuget: FSharp.Data" // => Reference FSharp.Data NuGet package
// => Provides JsonProvider
open FSharp.Data
type WeatherData = JsonProvider<"""
{
"temperature": 22.5,
"humidity": 65,
"conditions": "sunny"
}
"""> // => JsonProvider generates types from sample JSON
// => Creates WeatherData type with properties
// => IntelliSense available for all fields
let jsonString = """
{
"temperature": 18.3,
"humidity": 72,
"conditions": "cloudy"
}
"""
let weather = WeatherData.Parse(jsonString)
// => Parse JSON string into typed object
// => weather has compile-time properties
printfn "Temperature: %.1f°C" weather.Temperature
// => weather.Temperature is float (type-safe)
// => Outputs: Temperature: 18.3°C
printfn "Humidity: %d%%" weather.Humidity
// => weather.Humidity is int
// => Outputs: Humidity: 72%
printfn "Conditions: %s" weather.Conditions
// => weather.Conditions is string
// => Outputs: Conditions: cloudy
// Array handling:
type WeatherArray = JsonProvider<"""
[
{ "city": "Jakarta", "temp": 28.5 },
{ "city": "Bandung", "temp": 22.0 }
]
""">
let citiesJson = """
[
{ "city": "Jakarta", "temp": 29.1 },
{ "city": "Bandung", "temp": 21.5 },
{ "city": "Surabaya", "temp": 30.2 }
]
"""
let cities = WeatherArray.Parse(citiesJson)
// => cities is array of typed objects
for city in cities do // => Iterate 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 definitions and runtime reflection. API clients accessing weather services get immediate compile errors when JSON schemas change, catching breaking changes before deployment instead of discovering them in production.
Example 39: Type Providers - CSV
CSV type provider generates strongly-typed accessors for CSV data with automatic type inference.
// Example 39: Type Providers - CSV
open FSharp.Data
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
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
"""
let stocks = StockData.Parse(csvData)
// => Parse CSV into typed rows
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
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)
// => Sum volumes (type: int)
// => totalVolume is 78000000
let avgClose = stocks.Rows
|> Seq.averageBy (fun row -> row.Close)
// => Average closing prices (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 indexing and runtime parsing. Financial analysts process trading data with compile-time column verification, preventing production errors from typos like row.Volumne (typo) caught at compile time instead of runtime.
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 unit conversion errors like the Mars Climate Orbiter failure (pound-force vs. newtons). Physics simulations and engineering calculations use units to eliminate conversion errors, with compiler rejecting invalid operations like adding meters to kilograms.
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)
let parts = email.Split('@')
// => Split email at @
if parts.Length = 2 then
(parts.[0], parts.[1])
// => Return (username, domain) tuple
else
("", "") // => Invalid email: empty parts
let analyzeEmail email =
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: "
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
// => Takes multiplier, extracts from value
value * multiplier // => Returns multiplied value
let describeMultiple value =
match value with
| Multiplied 2 doubled ->
// => Multiplied 2 extracts value*2
// => doubled is value*2
sprintf "Doubled: %d" doubled
| _ -> ""
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 pattern matching. HTTP request parsers extract headers, query strings, and bodies using custom patterns, making routing logic read like declarative specifications instead of imperative parsing code.
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
// => (|Case1|Case2|...|) defines cases
if value % 2 = 0 then Even
// => Even case (no data)
else Odd // => Odd case (no data)
let describe value =
match value with // => Pattern match using active pattern
| Even -> // => Matches even numbers
sprintf "%d is even" value
| Odd -> // => Matches odd numbers
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
if value > 0 then Positive value
// => Positive case carries value
elif value < 0 then Negative -value
// => Negative case carries absolute value
else Zero // => Zero case (no data)
let describeNumber value =
match value with
| Positive v -> // => Extract positive value
sprintf "+%d (positive)" v
| Negative v -> // => Extract absolute of negative
sprintf "-%d (negative)" v
| Zero -> // => Zero case
"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 parameter
if value % divisor = 0 then
DivisibleBy (value / divisor)
// => Carries quotient
else
NotDivisibleBy // => Not divisible
let checkDivisibility value =
match value with
| DivisibleBy 3 quotient ->
// => Check divisible by 3
sprintf "%d / 3 = %d" value quotient
| NotDivisibleBy ->
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 beyond type system. Compiler pattern matchers classify tokens (Keyword, Identifier, Literal) using active patterns, making lexer code declarative and exhaustive-checking at compile time.
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 syntax for complex list transformations. Data scientists generate parameter combinations for grid search using comprehensions, creating thousands of test configurations in 3-4 lines instead of nested for loops with manual list building.
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. Search engines use sets to store unique document IDs, with union/intersection operations implementing boolean search queries (AND/OR/NOT) in O(n) time with structural sharing reducing memory usage.
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. Data structure libraries implement generic List.map, List.filter once for all types instead of separate mapInt, mapString functions, reducing codebase size while maintaining type safety.
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. Compilers traverse abstract syntax trees recursively, with pattern matching on node types, making code structure mirror data structure for clarity.
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. Parsers processing deeply nested JSON (1000+ levels) use tail-recursive descent without stack limits, where non-tail recursion would crash after ~1000 depth on typical JVM/CLR stacks.
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
if n = 0 then // => Base case
printfn "Done"
else
printfn "Even: %d" n
// => Print even number
processOdd (n - 1)
// => Transition to odd state
and processOdd n = // => Processes odd state
if n = 0 then // => Base case
printfn "Done"
else
printfn "Odd: %d" n
// => Print odd number
processEven (n - 1)
// => Transition to even state
processEven 5 // => Start with even state
// => Outputs:
// => Even: 5
// => Odd: 4
// => Even: 3
// => Odd: 2
// => Even: 1
// => Done
// Mutual recursion for expression evaluation:
type Expr =
| Number of int
| Add of Expr * Expr
| Multiply of Expr * Expr
let rec evalExpr expr = // => Evaluate expression
match expr with
| Number n -> n // => Base case: return number
| Add(left, right) ->
evalTerm left + evalTerm right
// => Addition: evaluate both sides
| Multiply(left, right) ->
evalExpr left * evalExpr right
and evalTerm term = // => Evaluate term (for precedence)
evalExpr term // => Calls back to evalExpr
// => Demonstrates mutual recursion pattern
let expression = Add(Number 10, Multiply(Number 5, Number 3))
// => 10 + (5 * 3)
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 grammar rules. Parsers define mutually recursive functions for language constructs: parseStatement calls parseExpression which calls parseStatement for nested structures, naturally expressing language grammar.
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. Dynamic programming solutions (knapsack, edit distance) use memoization to reduce exponential time complexity to polynomial, making previously intractable problems solvable in seconds.
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 without classes, reducing boilerplate. Large F# codebases organize thousands of functions into hierarchical modules (Domain.Pricing.Calculate) with clear boundaries, improving discoverability compared to flat function lists.
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
// => Provides hierarchical organization
module StringHelpers = // => Module within namespace
let reverse (s: string) =
s.ToCharArray() |> Array.rev |> System.String
let capitalize (s: string) =
if s.Length = 0 then s
else s.[0..0].ToUpper() + s.[1..]
module MathHelpers = // => Another module in same namespace
let square x = x * x
let cube x = x * x * x
// Using namespaced modules:
open MyCompany.Utils // => Open namespace
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
let rev2 = reverse "F#" // => No StringHelpers. prefix
// => 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 across teams. Enterprise applications use namespaces like Contoso.Trading.Execution and Contoso.Trading.Reporting to isolate components, enabling parallel development without function name collisions.
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 library design. Public APIs expose minimal surface area while implementations use many private helpers, preventing internal refactoring from breaking client code.
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# integration with OOP-heavy .NET ecosystems. F# code can implement ASP.NET MVC controllers, Entity Framework models, and WPF ViewModels using class syntax while keeping business logic functional.
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
abstract member Area : unit -> float
// => Abstract method (no implementation)
abstract member Perimeter : unit -> float
type Circle(radius: float) =
// => Class implementing interface
interface IShape with
// => Explicit interface implementation
member this.Area() =
System.Math.PI * radius * radius
// => Implementation of Area
member this.Perimeter() =
2.0 * System.Math.PI * radius
// => Implementation of Perimeter
type Rectangle(width: float, height: float) =
interface IShape with
member this.Area() =
width * height
member this.Perimeter() =
2.0 * (width + height)
// Using interfaces:
let circle = Circle(5.0) :> IShape
// => Upcast to interface type with :>
// => circle type: IShape
let rect = Rectangle(4.0, 6.0) :> IShape
// => rect type: IShape
let shapes = [ circle; rect ]
// => List of IShape (polymorphic)
for shape in shapes do // => Iterate over polymorphic collection
printfn "Area: %.2f, Perimeter: %.2f"
(shape.Area()) // => Call interface method
(shape.Perimeter())
// => Outputs:
// => Area: 78.54, Perimeter: 31.42
// => Area: 24.00, Perimeter: 20.00
// Interface with properties:
type IPerson =
abstract member Name : string
// => Property (no unit parameter)
abstract member Age : int
type Employee(name: string, age: int, salary: float) =
interface IPerson with
member this.Name = name
member this.Age = age
member this.Salary = salary
// => Additional member not in interface
let emp = Employee("Bob", 35, 75000.0)
let person = emp :> IPerson
// => Upcast to IPerson
printfn "Person: %s, %d" person.Name person.Age
// => Outputs: Person: Bob, 35
// printfn "Salary: %.0f" person.Salary
// => 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. Application layers depend on IRepository interface instead of concrete database implementations, allowing test doubles (mocks) to replace real databases during unit testing without code changes.
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
// Object expression implementing interface:
let consoleLogger = // => Create ILogger implementation inline
{ new ILogger with // => Object expression syntax
member this.Log(message) =
printfn "[LOG] %s" message }
// => 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
{ new System.IDisposable with
member this.Dispose() =
count <- count + 1
printfn "Disposed %d times" count }
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
counter2.Dispose() // => Outputs: Disposed 1 times
// Passing object expression to function:
let useLogger (logger: ILogger) message =
logger.Log(message) // => Use logger interface
useLogger consoleLogger "Processing data"
// => Outputs: [LOG] Processing data
useLogger
{ new ILogger with
member this.Log(msg) =
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 interface implementations for callbacks and event handlers. GUI frameworks use object expressions for event listeners, avoiding ceremonial class definitions for simple event handlers.
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 DUs naturally model hierarchical data. Compilers represent abstract syntax trees (AST) as recursive DUs, with pattern matching traversing syntax naturally: match expr with | Add(left, right) -> ... reads like grammar rules.
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 = {
FirstName: string
LastName: string
Age: int
City: string
}
let alice = {
FirstName = "Alice"
LastName = "Smith"
Age = 30
City = "Jakarta"
} // => Original record
printfn "%A" alice // => Outputs: { FirstName = "Alice"; LastName = "Smith"; Age = 30; City = "Jakarta" }
// Update single field:
let aliceOlder = { alice with Age = 31 }
// => 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" }
// => 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
let aliceRenamedMoved = { aliceRenamed with City = "Surabaya" }
// => Chain updates (immutable style)
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
// Functional state management:
let updateAge person newAge =
// => Function returning updated record
{ person with Age = newAge }
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 updates enable functional state management without mutation. Redux-style state stores use record updates to produce new application states, maintaining immutable history for time-travel debugging and undo/redo without complex memento patterns.
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 =
match n with
| 0 -> "zero" // => Literal pattern
| n when n < 0 -> // => Guard: when condition
"negative" // => n is bound, accessible in guard
| n when n < 10 -> // => Another guard
"small positive"
| n when n < 100 ->
"medium positive"
| _ ->
"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 =
match list with
| [] -> "empty" // => Empty list pattern
| [x] when x > 0 -> // => Single-element list with guard
"single positive"
| [x] when x < 0 ->
"single negative"
| [x] -> // => Single element (any value)
"single zero"
| head :: tail when head = 0 ->
// => Starts with 0
"starts with zero"
| head :: tail when List.length tail > 5 ->
// => Long list (more than 6 elements)
"long list"
| _ ->
"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) =
match (x, y) with
| (a, b) when a = b ->
// => Both equal
"equal"
| (a, b) when a + b = 10 ->
// => Sum is 10
"sum to 10"
| (a, b) when a > b ->
// => First larger
"first larger"
| _ ->
"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: Guards enable complex matching logic without nested if statements. Business rule engines match orders with guards: | Order(amount=a) when a > 10000 -> ApplyBulkDiscount reads like natural language specifications.
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
| _ -> false // => Non-empty case
printfn "[] empty: %b" (isEmptyList [])
// => Outputs: [] empty: true
printfn "[1] empty: %b" (isEmptyList [1])
// => Outputs: [1] empty: false
// Multiple cases:
let describeList = function
| [] -> "empty"
| [x] -> sprintf "single: %d" x
| [x; y] -> sprintf "pair: %d, %d" x y
| head :: tail -> sprintf "list starting with %d" head
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
| (x, y) -> x + y // => Deconstruct tuple in pattern
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 function pattern
match list with
| [] -> 0
| head :: tail -> head + listSum tail
// Equivalent using function keyword:
let rec listSum2 = function
| [] -> 0
| head :: tail -> head + listSum2 tail
let total1 = listSum [1; 2; 3; 4; 5]
// => total1 is 15
let total2 = listSum2 [1; 2; 3; 4; 5]
// => total2 is 15
printfn "Total1: %d" total1
// => Outputs: Total1: 15
printfn "Total2: %d" total2
// => Outputs: Total2: 15
// Option pattern:
let describeOption = function
| Some value -> sprintf "Value: %d" value
| None -> "No value"
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: Function patterns reduce boilerplate for common matching scenarios. List processing functions use function | [] -> ... | head::tail -> ... consistently, making code readable through consistent patterns across codebase.
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 loops and accumulator logic. Financial reporting pipelines group transactions by category, partition by date range, and collect nested data in 3-4 lines instead of 50+ lines of imperative accumulation code.
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.