Advanced
This advanced tutorial covers F#’s expert-level patterns and functional architecture through 25 heavily annotated examples. Topics include mailbox processors for concurrency, statically resolved type parameters (SRTP), quotations for metaprogramming, advanced computation expressions, and domain-driven design patterns that make illegal states unrepresentable.
Example 61: MailboxProcessor - Agent-Based Concurrency
MailboxProcessor implements the actor model for concurrency, where agents process messages sequentially from a mailbox, eliminating race conditions without locks.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
A[Client Posts<br/>Messages]:::blue --> B[Mailbox Queue]:::orange
B --> C[Agent Processes<br/>Sequentially]:::teal
C --> D[State Updated]:::purple
D -->|loop| C
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 61: MailboxProcessor - Agent-Based Concurrency
type Message =
| Increment
| Decrement
| GetCount of AsyncReplyChannel<int>
// => Message type with async reply channel
// => GetCount carries channel for returning result
let counter = MailboxProcessor.Start(fun inbox ->
// => MailboxProcessor.Start creates agent
// => inbox is the mailbox receiving messages
let rec loop count =
// => Recursive loop maintaining state
// => count is current state (no mutable variable!)
async {
let! msg = inbox.Receive()
// => let! awaits next message
// => Blocks until message arrives
match msg with
| Increment ->
// => Increment case
return! loop (count + 1)
// => Recurse with incremented count
// => return! is tail recursion (no stack growth)
| Decrement ->
// => Decrement case
return! loop (count - 1)
// => Recurse with decremented count
| GetCount replyChannel ->
// => GetCount case with reply channel
replyChannel.Reply(count)
// => Send current count back via channel
return! loop count
// => Continue with same count
}
loop 0 // => Start loop with initial state 0
)
// Post messages (fire-and-forget)
counter.Post(Increment) // => Posts Increment message
counter.Post(Increment) // => Posts another Increment
counter.Post(Decrement) // => Posts Decrement
// Query current state
let currentCount = counter.PostAndReply(fun replyChannel -> GetCount replyChannel)
// => PostAndReply sends message and waits for reply
// => Creates reply channel automatically
// => currentCount is 1 (2 increments - 1 decrement)
printfn "Count: %d" currentCount
// => Outputs: Count: 1Key Takeaway: MailboxProcessor provides actor-model concurrency with sequential message processing, eliminating race conditions through isolated state and message-passing instead of shared memory.
Why It Matters: Actor-based concurrency prevents data races without locks, deadlocks, or complex synchronization primitives. MailboxProcessor agents can process millions of concurrent updates with zero race conditions, achieving throughput improvements over lock-based implementations while simplifying debugging through message tracing.
Example 62: Agent-Based State Management
Agents encapsulate mutable state safely, enabling concurrent updates without explicit locking.
// Example 62: Agent-Based State Management
type BankMessage =
| Deposit of amount: float
| Withdraw of amount: float * AsyncReplyChannel<Result<float, string>>
| GetBalance of AsyncReplyChannel<float>
let createBankAccount initialBalance =
// => Factory function creating bank account agent
MailboxProcessor.Start(fun inbox ->
let rec loop balance =
// => Loop maintains current balance
async {
let! msg = inbox.Receive()
match msg with
| Deposit amount ->
return! loop (balance + amount)
// => Add deposit to balance
| Withdraw (amount, reply) ->
// => Withdraw with reply channel
if balance >= amount then
reply.Reply(Ok (balance - amount))
// => Success: reply with new balance
return! loop (balance - amount)
// => Continue with updated balance
else
reply.Reply(Error "Insufficient funds")
// => Error: insufficient funds
return! loop balance
// => Balance unchanged
| GetBalance reply ->
reply.Reply(balance)
// => Return current balance
return! loop balance
}
loop initialBalance
)
let account = createBankAccount 1000.0
// => Create account with $1000 initial balance
account.Post(Deposit 500.0)
// => Deposit $500 (balance: $1500)
let withdrawResult = account.PostAndReply(fun reply -> Withdraw (300.0, reply))
// => Attempt to withdraw $300
match withdrawResult with
| Ok newBalance ->
printfn "Withdrawal successful, new balance: %.2f" newBalance
// => Outputs: Withdrawal successful, new balance: 1200.00
| Error msg ->
printfn "Withdrawal failed: %s" msg
let balance = account.PostAndReply(GetBalance)
// => Query current balance
printfn "Current balance: %.2f" balance
// => Outputs: Current balance: 1200.00Key Takeaway: Agents encapsulate state and enforce sequential access through message passing, preventing concurrent modification bugs without explicit locks.
Why It Matters: Agent-based state management eliminates many concurrency bugs, as state mutations are serialized automatically. Financial systems use agents to manage account balances with zero race conditions, processing thousands of concurrent transactions per second with correctness guarantees impossible in lock-based systems.
Example 63: Async Parallelism with Async.Parallel
Async.Parallel executes multiple async computations concurrently and collects results, enabling parallel I/O without blocking threads.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
A[Async.Parallel]:::blue --> B[Task 1<br/>runs]:::orange
A --> C[Task 2<br/>runs]:::orange
A --> D[Task 3<br/>runs]:::orange
B --> E[Collect Results]:::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 63: Async Parallelism with Async.Parallel
let fetchData url = async {
// => Simulates HTTP fetch
do! Async.Sleep 1000 // => Simulate network delay
return sprintf "Data from %s" url
// => Return fetched data
}
let urls = [
"https://api1.example.com"
"https://api2.example.com"
"https://api3.example.com"
]
let fetchAll = async {
let! results = urls
|> List.map fetchData
// => Map each URL to async operation
// => Creates list of Async<string>
|> Async.Parallel
// => Execute all async ops in parallel
// => Returns Async<string array>
return results // => results is string array
}
let allData = fetchAll |> Async.RunSynchronously
// => Run async and wait for completion
// => allData is string array
for data in allData do
printfn "%s" data
// => Outputs:
// => Data from https://api1.example.com
// => Data from https://api2.example.com
// => Data from https://api3.example.com
// => (executes in ~1 second, not 3)Key Takeaway: Async.Parallel executes async operations concurrently and collects results into an array, enabling efficient parallel I/O without thread blocking.
Why It Matters: Parallel async operations reduce latency by factor of N for independent I/O operations. Microservices aggregate data from multiple backend APIs using Async.Parallel, achieving faster response times, improving user experience while maintaining thread pool efficiency.
Example 64: Custom Computation Expression Builders
Computation expression builders define custom control flow syntax, enabling domain-specific languages (DSLs) embedded in F#.
// Example 64: Custom Computation Expression Builders
type LoggingBuilder() =
// => Custom builder for logging workflow
member _.Bind(x, f) =
// => Bind enables let! syntax
// => x is value, f is continuation function
printfn "Binding value: %A" x
// => Log the binding
f x // => Apply continuation
member _.Return(x) =
// => Return enables return syntax
printfn "Returning value: %A" x
// => Log the return
x // => Return value
member _.ReturnFrom(x) =
// => ReturnFrom enables return! syntax
printfn "Returning from: %A" x
x
member _.Zero() =
// => Zero handles empty branches
printfn "Zero"
()
let logging = LoggingBuilder()
// => Create builder instance
// => Enables logging { ... } syntax
let result = logging {
// => Computation expression using custom builder
let! x = 10 // => Triggers Bind(10, ...)
// => Outputs: Binding value: 10
let! y = 20 // => Triggers Bind(20, ...)
// => Outputs: Binding value: 20
let sum = x + y // => Regular let (no logging)
return sum // => Triggers Return(30)
// => Outputs: Returning value: 30
} // => result is 30
printfn "Final result: %d" result
// => Outputs: Final result: 30Key Takeaway: Computation expression builders define custom control flow by implementing Bind, Return, and other members, creating domain-specific syntax embedded in F#.
Why It Matters: Custom builders enable domain-specific languages without leaving F#. Financial modeling libraries use builders to create pricing workflows with automatic risk calculations, validation, and logging, making complex financial logic read like business rules while maintaining type safety and composability.
Example 65: Query Expressions (LINQ Integration)
Query expressions integrate LINQ into F#, providing SQL-like syntax for data querying with compile-time type checking.
// Example 65: Query Expressions (LINQ Integration)
type Person = { Name: string; Age: int; City: string }
let people = [
{ Name = "Alice"; Age = 30; City = "New York" }
{ Name = "Bob"; Age = 25; City = "London" }
{ Name = "Charlie"; Age = 35; City = "New York" }
{ Name = "Diana"; Age = 28; City = "Paris" }
]
let newYorkers = query {
// => query { } is built-in computation expression
for person in people do
// => Iterate over people
where (person.City = "New York")
// => Filter by city
sortBy person.Age // => Sort by age
select person // => Select full person record
} // => Returns IQueryable<Person>
printfn "New Yorkers:"
for person in newYorkers do
printfn "%s, age %d" person.Name person.Age
// => Outputs:
// => Alice, age 30
// => Charlie, age 35
// Grouping query
let byCity = query {
for person in people do
groupBy person.City into cityGroup
// => Group by city
select (cityGroup.Key, cityGroup.Count())
// => Select city and count
}
printfn "\nPeople by city:"
for (city, count) in byCity do
printfn "%s: %d" city count
// => Outputs:
// => New York: 2
// => London: 1
// => Paris: 1Key Takeaway: Query expressions provide SQL-like syntax for querying collections with compile-time type checking, integrating F# with LINQ for database and collection operations.
Why It Matters: Query expressions enable type-safe database queries that compile to SQL, preventing runtime errors from typos or schema changes. Enterprise applications use query expressions to access SQL databases with 100% type safety, catching bugs at compile time that would manifest as runtime exceptions in string-based SQL.
Example 66: Quotations - Code as Data
Quotations represent F# code as data structures, enabling metaprogramming, code analysis, and runtime code generation.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
A[Source Code]:::blue --> B[Quotation<br/>AST]:::orange
B --> C[Analyze]:::teal
B --> D[Transform]:::teal
B --> E[Execute]:::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:#029E73,stroke:#000,color:#fff
style E fill:#029E73,stroke:#000,color:#fff
Code:
// Example 66: Quotations - Code as Data
open FSharp.Quotations
open FSharp.Quotations.Patterns
let simpleExpr = <@ 1 + 2 @>
// => <@ ... @> creates quotation
// => Captures expression as AST
// => Type: Expr<int>
let inspectQuotation (expr: Expr) =
// => Function analyzing quotation
match expr with
| Call (None, methodInfo, args) ->
// => Pattern match on method call
printfn "Method call: %s with %d arguments" methodInfo.Name args.Length
| Value (value, typ) ->
// => Pattern match on value
printfn "Value: %A of type %s" value typ.Name
| _ ->
printfn "Other expression: %A" expr
inspectQuotation simpleExpr
// => Analyzes 1 + 2 quotation
// More complex quotation
let complexExpr = <@ fun x -> x * 2 + 1 @>
// => Lambda quotation
let rec printExpr (expr: Expr) =
// => Recursive quotation printer
match expr with
| Lambda (var, body) ->
printfn "Lambda: %s => ..." var.Name
printExpr body // => Recurse into body
| Call (target, methodInfo, args) ->
printfn "Call: %s" methodInfo.Name
args |> List.iter printExpr
| Value (value, _) ->
printfn "Value: %A" value
| Var var ->
printfn "Variable: %s" var.Name
| _ ->
printfn "Expression: %A" expr
printExpr complexExpr
// => Outputs:
// => Lambda: x => ...
// => Call: op_Addition
// => Call: op_Multiply
// => Variable: x
// => Value: 2
// => Value: 1Key Takeaway: Quotations capture F# code as abstract syntax trees (AST), enabling code analysis, transformation, and runtime generation through pattern matching on expression structure.
Why It Matters: Quotations enable compiler-verified code generation and domain-specific optimizations. Type providers use quotations to generate optimized database queries from F# expressions, translating <@ person.Age > 25 @> into SQL WHERE Age > 25 with compile-time verification, achieving native database performance with type safety.
Example 67: Reflection and Type Information
F# reflection provides runtime type inspection and metadata access, enabling generic algorithms and dynamic behavior.
// Example 67: Reflection and Type Information
open System.Reflection
type Product = { Id: int; Name: string; Price: float }
let productType = typeof<Product>
// => Gets Type object for Product
// => Type: System.Type
printfn "Type name: %s" productType.Name
// => Outputs: Type name: Product
let properties = productType.GetProperties()
// => Get all properties via reflection
// => Returns PropertyInfo array
for prop in properties do
printfn "Property: %s (type: %s)" prop.Name prop.PropertyType.Name
// => Outputs:
// => Property: Id (type: Int32)
// => Property: Name (type: String)
// => Property: Price (type: Double)
// Dynamic property access
let product = { Id = 1; Name = "Widget"; Price = 9.99 }
let getPropertyValue (obj: obj) (propName: string) =
// => Generic property getter
let typ = obj.GetType()
let prop = typ.GetProperty(propName)
// => Get property by name
prop.GetValue(obj) // => Get property value dynamically
let name = getPropertyValue product "Name"
// => Dynamically access Name property
// => name is "Widget" (type: obj)
printfn "Product name: %A" name
// => Outputs: Product name: "Widget"
// Generic printing
let printRecord (record: 'T) =
// => Generic record printer
let typ = typeof<'T>
printfn "%s:" typ.Name
for prop in typ.GetProperties() do
let value = prop.GetValue(record)
printfn " %s = %A" prop.Name value
printRecord product
// => Outputs:
// => Product:
// => Id = 1
// => Name = "Widget"
// => Price = 9.99Key Takeaway: Reflection provides runtime type inspection and dynamic property access, enabling generic algorithms that work across types without compile-time knowledge.
Why It Matters: Reflection enables serialization libraries, ORMs, and testing frameworks to work generically across all F# types. JSON serializers use reflection to convert any F# record to JSON automatically, eliminating 90% of boilerplate code required in languages without powerful reflection while maintaining type safety at compile time.
Example 68: Advanced Type Constraints with SRTP
Statically Resolved Type Parameters (SRTP) enable compile-time polymorphism through inline functions and type constraints.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
A[Generic Code<br/>inline add]:::blue --> B[Compile Time<br/>Type Resolution]:::orange
B --> C[int + int]:::teal
B --> D[float + float]:::teal
B --> E[string + string]:::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:#029E73,stroke:#000,color:#fff
style E fill:#029E73,stroke:#000,color:#fff
Code:
// Example 68: Advanced Type Constraints with SRTP
let inline add x y =
// => inline enables SRTP
// => No type annotation needed
x + y // => + operator constraint
// => Works for ANY type with + operator
let intSum = add 10 20 // => int addition (30)
let floatSum = add 3.5 2.1
// => float addition (5.6)
let stringConcat = add "Hello" " World"
// => string concatenation
printfn "%d" intSum // => Outputs: 30
printfn "%.1f" floatSum // => Outputs: 5.6
printfn "%s" stringConcat // => Outputs: Hello World
// Explicit SRTP constraint
let inline sumList< ^T when ^T : (static member Zero : ^T)
and ^T : (static member (+) : ^T * ^T -> ^T)>
// => ^T is SRTP type parameter
// => Requires Zero static member
// => Requires + operator
(items: ^T list) : ^T =
List.fold (+) LanguagePrimitives.GenericZero items
// => GenericZero works with SRTP
let intList = [1; 2; 3; 4; 5]
let intTotal = sumList intList
// => intTotal is 15
let floatList = [1.5; 2.5; 3.0]
let floatTotal = sumList floatList
// => floatTotal is 7.0
printfn "Int total: %d" intTotal
// => Outputs: Int total: 15
printfn "Float total: %.1f" floatTotal
// => Outputs: Float total: 7.0Key Takeaway: SRTP with inline functions enables compile-time polymorphism through operator and member constraints, creating generic code that works across types without runtime overhead.
Why It Matters: SRTP provides zero-cost abstractions impossible in languages with runtime generics. Numerical libraries use SRTP to write algorithms once that work efficiently for int, float, BigInteger, and custom numeric types, achieving C++ template-level performance while maintaining F#’s type safety and avoiding code duplication.
Example 69: Custom Operators and Operator Overloading
F# allows defining custom operators for domain-specific syntax, creating expressive DSLs.
// Example 69: Custom Operators and Operator Overloading
type Vector = { X: float; Y: float }
// Custom operators for vector math
let (+.) v1 v2 =
// => Custom operator: +.
// => Vector addition
{ X = v1.X + v2.X; Y = v1.Y + v2.Y }
let (-.) v1 v2 =
// => Custom operator: -.
// => Vector subtraction
{ X = v1.X - v2.X; Y = v1.Y - v2.Y }
let ( *. ) scalar v =
// => Custom operator: *.
// => Scalar multiplication
{ X = scalar * v.X; Y = scalar * v.Y }
let v1 = { X = 3.0; Y = 4.0 }
let v2 = { X = 1.0; Y = 2.0 }
let sum = v1 +. v2 // => Use custom + operator
// => sum is { X = 4.0; Y = 6.0 }
let diff = v1 -. v2 // => Use custom - operator
// => diff is { X = 2.0; Y = 2.0 }
let scaled = 2.0 *. v1 // => Use custom * operator
// => scaled is { X = 6.0; Y = 8.0 }
printfn "Sum: %A" sum
printfn "Diff: %A" diff
printfn "Scaled: %A" scaled
// Pipeline operator alternative
let (|>>) x f = f x |> List.map
// => Custom pipeline operator
// => Combines pipe and map
let numbers = [1; 2; 3; 4; 5]
let doubled = numbers |>> (fun x -> x * 2)
// => Uses custom pipeline
// => doubled is [2; 4; 6; 8; 10]
printfn "Doubled: %A" doubledKey Takeaway: Custom operators create domain-specific syntax for types, enabling expressive code that reads like mathematical notation while maintaining type safety.
Why It Matters: Custom operators make domain logic self-documenting. Physics simulations use vector operators (v1 +. v2) that mirror mathematical notation exactly, reducing transcription errors compared to method calls (v1.Add(v2)) while improving readability for scientists reviewing code.
Example 70: Parameterized Active Patterns
Parameterized active patterns accept arguments, enabling flexible pattern matching with context.
// Example 70: Parameterized Active Patterns
let (|DivisibleBy|_|) divisor n =
// => Parameterized partial active pattern
// => divisor is parameter
// => n is value being matched
if n % divisor = 0 then
Some (n / divisor)
// => Return Some with quotient
else
None // => Return None if not divisible
let describe n =
match n with
| DivisibleBy 15 quotient ->
// => Pass 15 as divisor parameter
sprintf "%d is divisible by 15 (quotient: %d)" n quotient
| DivisibleBy 3 quotient ->
// => Pass 3 as divisor parameter
sprintf "%d is divisible by 3 (quotient: %d)" n quotient
| DivisibleBy 5 quotient ->
// => Pass 5 as divisor parameter
sprintf "%d is divisible by 5 (quotient: %d)" n quotient
| _ ->
sprintf "%d is not divisible by 3, 5, or 15" n
printfn "%s" (describe 15)
// => Outputs: 15 is divisible by 15 (quotient: 1)
printfn "%s" (describe 9)
// => Outputs: 9 is divisible by 3 (quotient: 3)
printfn "%s" (describe 10)
// => Outputs: 10 is divisible by 5 (quotient: 2)
printfn "%s" (describe 7)
// => Outputs: 7 is not divisible by 3, 5, or 15
// Regex parameterized pattern
open System.Text.RegularExpressions
let (|Regex|_|) pattern input =
// => Regex active pattern
let m = Regex.Match(input, pattern)
if m.Success then
Some (List.tail [ for g in m.Groups -> g.Value ])
// => Return captured groups
else
None
let parseEmail email =
match email with
| Regex @"(.+)@(.+)\.(.+)" [user; domain; tld] ->
// => Match email pattern with captures
sprintf "User: %s, Domain: %s.%s" user domain tld
| _ ->
"Invalid email"
printfn "%s" (parseEmail "alice@example.com")
// => Outputs: User: alice, Domain: example.comKey Takeaway: Parameterized active patterns accept arguments for flexible pattern matching, enabling patterns that adapt to context like divisibility checks or regex matching.
Why It Matters: Parameterized patterns eliminate repetitive conditional logic. Parsing libraries use regex patterns to extract data from structured text, reducing string manipulation code to declarative pattern matches that are easier to test and maintain.
Example 71: Type Extensions and Augmentations
Type extensions add members to existing types, including types from external libraries, without inheritance.
// Example 71: Type Extensions and Augmentations
// Intrinsic extension (same file as type definition)
type Person = { Name: string; Age: int }
type Person with
// => Intrinsic extension
// => Adds members to Person type
member this.IsAdult = this.Age >= 18
// => Computed property
member this.Greet() = sprintf "Hello, I'm %s" this.Name
// => Instance method
let alice = { Name = "Alice"; Age = 30 }
printfn "%s" (alice.Greet())
// => Outputs: Hello, I'm Alice
printfn "Is adult: %b" alice.IsAdult
// => Outputs: Is adult: true
// Optional extension (any file)
module StringExtensions =
type System.String with
// => Extend .NET String type
member this.Reverse() =
// => New method on String
System.String(this.ToCharArray() |> Array.rev)
member this.IsPalindrome() =
// => Another extension method
this = this.Reverse()
open StringExtensions
let text = "hello"
let reversed = text.Reverse()
// => Call extension method
// => reversed is "olleh"
printfn "Reversed: %s" reversed
let palindrome = "racecar"
printfn "Is '%s' a palindrome? %b" palindrome (palindrome.IsPalindrome())
// => Outputs: Is 'racecar' a palindrome? true
// Extension with operators
type System.Int32 with
// => Extend int type
member this.IsEven() = this % 2 = 0
member this.IsOdd() = this % 2 <> 0
printfn "Is 10 even? %b" (10.IsEven())
// => Outputs: Is 10 even? trueKey Takeaway: Type extensions add members to existing types without inheritance, enabling extension of .NET types and third-party libraries with custom functionality.
Why It Matters: Type extensions adapt external libraries to domain needs without wrapper classes. Enterprise applications extend .NET types like DateTime with domain-specific methods (date.IsBusinessDay(), date.NextQuarter()), improving readability while avoiding the performance cost and ceremony of wrapper objects.
Example 72: Units of Measure - Type-Safe Calculations
Units of measure provide compile-time dimensional analysis, preventing unit conversion errors in calculations.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
A[100.0 meter]:::blue --> B[/ time<br/>5.0 second]:::orange
B --> C[20.0 meter/second]:::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
Code:
// Example 72: Units of Measure - Type-Safe Calculations
[<Measure>] type meter
[<Measure>] type second
[<Measure>] type kg
let distance = 100.0<meter>
// => distance has type float<meter>
// => Type system tracks units
let time = 5.0<second> // => time has type float<second>
let speed = distance / time
// => speed has type float<meter/second>
// => Unit division computed automatically
printfn "Speed: %.1f m/s" speed
// => Outputs: Speed: 20.0 m/s
// => Units erased at runtime (zero cost)
// Type error prevention
// let invalid = distance + time
// => COMPILE ERROR: Cannot add meter + second
// => Type system prevents nonsensical operations
// Unit conversion
[<Measure>] type cm
let toCentimeters (m: float<meter>) : float<cm> =
// => Explicit unit conversion
m * 100.0<cm/meter> // => Multiply by conversion factor
let distanceCm = toCentimeters distance
// => distanceCm has type float<cm>
// => distanceCm is 10000.0<cm>
printfn "Distance: %.0f cm" distanceCm
// Derived units
let mass = 50.0<kg>
let force = mass * (distance / (time * time))
// => force has type float<kg meter/second^2>
// => Newton's second law: F = ma
printfn "Force: %.1f N" force
// Generic functions with units
let add< [<Measure>] 'u > (x: float<'u>) (y: float<'u>) : float<'u> =
// => Generic over unit of measure
// => 'u is unit type parameter
x + y
let total = add 10.0<meter> 20.0<meter>
// => total is 30.0<meter>Key Takeaway: Units of measure provide compile-time dimensional analysis with zero runtime cost, preventing unit conversion errors through type system enforcement.
Why It Matters: The Mars Climate Orbiter was lost due to metric/imperial unit confusion. Units of measure eliminate this entire class of errors at compile time with zero runtime overhead, making F# ideal for scientific computing, physics simulations, and financial calculations where unit correctness is critical.
Example 73: Phantom Types for Type-State Pattern
Phantom types encode state in type system, making illegal state transitions impossible at compile time.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC
graph TD
A[Connection<Closed>]:::blue --> B[open]:::orange
B --> C[Connection<Open>]:::teal
C --> D[query]:::orange
D --> E[Result]:::purple
C --> F[close]:::orange
F --> A
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:#CC78BC,stroke:#000,color:#fff
style F fill:#DE8F05,stroke:#000,color:#fff
Code:
// Example 73: Phantom Types for Type-State Pattern
type Closed = class end // => Phantom type (no instances)
type Open = class end // => Phantom type
type Connection<'state> = private {
// => Generic over state
// => 'state is phantom (never instantiated)
ConnectionString: string
}
module Connection =
let create connectionString : Connection<Closed> =
// => Returns closed connection
{ ConnectionString = connectionString }
let open' (conn: Connection<Closed>) : Connection<Open> =
// => Accepts closed, returns open
// => Type system enforces state
printfn "Opening connection: %s" conn.ConnectionString
{ ConnectionString = conn.ConnectionString }
let query (sql: string) (conn: Connection<Open>) : string =
// => Requires open connection
// => COMPILE ERROR if connection closed
printfn "Executing: %s" sql
"Query result" // => Return result
let close (conn: Connection<Open>) : Connection<Closed> =
// => Accepts open, returns closed
printfn "Closing connection: %s" conn.ConnectionString
{ ConnectionString = conn.ConnectionString }
// Usage
let conn = Connection.create "Server=localhost"
// => conn has type Connection<Closed>
// let result = Connection.query "SELECT *" conn
// => COMPILE ERROR: conn is Connection<Closed>
// => query requires Connection<Open>
let openConn = Connection.open' conn
// => openConn has type Connection<Open>
let result = Connection.query "SELECT * FROM users" openConn
// => OK: openConn is Connection<Open>
// => Outputs: Executing: SELECT * FROM users
let closedConn = Connection.close openConn
// => closedConn has type Connection<Closed>
// let result2 = Connection.query "SELECT *" closedConn
// => COMPILE ERROR: closedConn is Connection<Closed>
printfn "Result: %s" resultKey Takeaway: Phantom types encode state in the type system, making illegal state transitions impossible at compile time through type-level state tracking.
Why It Matters: Phantom types eliminate entire categories of runtime errors. Database connection pools use phantom types to prevent querying closed connections, file APIs prevent reading closed files, and authentication systems prevent accessing resources without valid tokens—all enforced at compile time with zero runtime cost.
Example 74: GADTs Emulation with Discriminated Unions
F# emulates Generalized Algebraic Data Types (GADTs) using discriminated unions with type constraints.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
A[Expr Tree]:::blue --> B[TLit 10]:::orange
A --> C[TAdd]:::orange
C --> D[TLit 20]:::teal
C --> E[Eval: 30]:::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:#029E73,stroke:#000,color:#fff
style E fill:#029E73,stroke:#000,color:#fff
Code:
// Example 74: GADTs Emulation with Discriminated Unions
type Value<'a> =
| IntValue of int
| StringValue of string
| BoolValue of bool
let eval (value: Value<'a>) : 'a =
// => Generic return type
match value with
| IntValue i -> box i :?> 'a
// => Cast int to 'a
| StringValue s -> box s :?> 'a
// => Cast string to 'a
| BoolValue b -> box b :?> 'a
// => Cast bool to 'a
let intVal: int = eval (IntValue 42)
// => intVal is 42 (type: int)
let strVal: string = eval (StringValue "hello")
// => strVal is "hello" (type: string)
printfn "Int: %d" intVal
printfn "String: %s" strVal
// Better GADT emulation with type-safe evaluation
type Expr =
| Lit of int
| Add of Expr * Expr
| Eq of Expr * Expr
type 'a TypedExpr =
| TLit of int
| TAdd of int TypedExpr * int TypedExpr
| TEq of int TypedExpr * int TypedExpr
let rec evalTyped (expr: 'a TypedExpr) : 'a =
// => Type-safe evaluation
match expr with
| TLit i -> box i :?> 'a
| TAdd (e1, e2) ->
let v1 = evalTyped e1
let v2 = evalTyped e2
box (v1 + v2) :?> 'a
| TEq (e1, e2) ->
let v1 = evalTyped e1
let v2 = evalTyped e2
box (v1 = v2) :?> 'a
let expr = TAdd(TLit 10, TLit 20)
// => expr has type int TypedExpr
let result: int = evalTyped expr
// => result is 30
printfn "Expression result: %d" result
let eqExpr = TEq(TLit 5, TLit 5)
// => eqExpr has type int TypedExpr
let eqResult: bool = evalTyped eqExpr
// => eqResult is true
printfn "Equality result: %b" eqResultKey Takeaway: F# emulates GADTs using discriminated unions with type parameters and runtime casts, enabling type-safe expression evaluation with heterogeneous return types.
Why It Matters: GADT emulation enables building type-safe interpreters and compilers in F#. Expression evaluators use GADTs to ensure arithmetic expressions return int and boolean expressions return bool, catching type errors at compile time that would be runtime exceptions in dynamically typed languages.
Example 75: Functional Dependency Injection
Dependency injection in functional style uses higher-order functions and closures instead of IoC containers.
// Example 75: Functional Dependency Injection
type ILogger =
abstract member Log : string -> unit
type ConsoleLogger() =
interface ILogger with
member _.Log(msg) = printfn "LOG: %s" msg
type IDatabase =
abstract member Query : string -> string
type MockDatabase() =
interface IDatabase with
member _.Query(sql) = sprintf "Mock result for: %s" sql
// Service using dependency injection
type UserService(logger: ILogger, db: IDatabase) =
member _.GetUser(id: int) =
logger.Log(sprintf "Fetching user %d" id)
let result = db.Query(sprintf "SELECT * FROM users WHERE id = %d" id)
logger.Log(sprintf "Query returned: %s" result)
result
// Functional DI: Functions taking dependencies as parameters
let createUserService (logger: ILogger) (db: IDatabase) =
// => Factory function
fun (id: int) -> // => Returns function with dependencies captured
logger.Log(sprintf "Fetching user %d" id)
let result = db.Query(sprintf "SELECT * FROM users WHERE id = %d" id)
logger.Log(sprintf "Query returned: %s" result)
result
// Setup dependencies
let logger = ConsoleLogger() :> ILogger
let db = MockDatabase() :> IDatabase
// Object-oriented DI
let service = UserService(logger, db)
let user1 = service.GetUser(42)
// => Outputs: LOG: Fetching user 42
// => Outputs: LOG: Query returned: Mock result for: SELECT * FROM users WHERE id = 42
// Functional DI
let getUser = createUserService logger db
// => getUser is function with dependencies
// => getUser has type: int -> string
let user2 = getUser 99 // => Call function with injected dependencies
// => Outputs: LOG: Fetching user 99
printfn "User 1: %s" user1
printfn "User 2: %s" user2
// Reader monad for DI
type Reader<'env, 'a> = Reader of ('env -> 'a)
let runReader (Reader f) env = f env
let ask = Reader id // => Gets environment
let getUserReader userId = Reader(fun (logger: ILogger, db: IDatabase) ->
// => Reader computation capturing dependencies
logger.Log(sprintf "Fetching user %d" userId)
db.Query(sprintf "SELECT * FROM users WHERE id = %d" userId)
)
let user3 = runReader (getUserReader 123) (logger, db)
// => Run reader with environment
printfn "User 3: %s" user3Key Takeaway: Functional dependency injection uses higher-order functions and closures to inject dependencies, avoiding heavy IoC containers while maintaining testability and composition.
Why It Matters: Functional DI eliminates runtime reflection and container complexity. Microservices inject database connections, loggers, and configuration through function parameters, improving testability (easy to pass mocks) and performance (no reflection overhead) compared to traditional IoC containers.
Example 76: Event Sourcing Pattern
Event sourcing stores state changes as immutable events, enabling complete audit trails and time travel debugging.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC
graph TD
A[AccountCreated<br/>1000.0]:::blue --> B[MoneyDeposited<br/>+500]:::orange
B --> C[MoneyWithdrawn<br/>-200]:::teal
C --> D[Current State<br/>1300.0]:::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 76: Event Sourcing Pattern
type Event =
| AccountCreated of accountId: string * initialBalance: float
| MoneyDeposited of amount: float
| MoneyWithdrawn of amount: float
type AccountState = {
AccountId: string
Balance: float
}
let applyEvent state event =
// => State transition function
// => Pure function: state + event -> new state
match event with
| AccountCreated (id, balance) ->
{ AccountId = id; Balance = balance }
| MoneyDeposited amount ->
{ state with Balance = state.Balance + amount }
| MoneyWithdrawn amount ->
{ state with Balance = state.Balance - amount }
// Event stream (immutable history)
let events = [
AccountCreated ("ACC001", 1000.0)
MoneyDeposited 500.0
MoneyWithdrawn 200.0
MoneyDeposited 100.0
]
// Rebuild current state from events
let currentState = events |> List.fold applyEvent { AccountId = ""; Balance = 0.0 }
// => Fold applies each event sequentially
// => currentState is { AccountId = "ACC001"; Balance = 1400.0 }
printfn "Current state: %A" currentState
// => Outputs: Current state: {AccountId = "ACC001"; Balance = 1400.0}
// Time travel: state at any point in history
let stateAfterSecondEvent = events
|> List.take 2
|> List.fold applyEvent { AccountId = ""; Balance = 0.0 }
// => State after first 2 events
// => Balance is 1500.0 (1000 + 500)
printfn "State after 2 events: %A" stateAfterSecondEvent
// Projections: different views of same events
let totalDeposited = events
|> List.choose (function
| MoneyDeposited amt -> Some amt
| _ -> None)
|> List.sum
// => totalDeposited is 600.0 (500 + 100)
let totalWithdrawn = events
|> List.choose (function
| MoneyWithdrawn amt -> Some amt
| _ -> None)
|> List.sum
// => totalWithdrawn is 200.0
printfn "Total deposited: %.2f" totalDeposited
printfn "Total withdrawn: %.2f" totalWithdrawnKey Takeaway: Event sourcing stores state as immutable event streams, enabling complete audit trails, time travel debugging, and multiple projections from the same event history.
Why It Matters: Event sourcing provides complete audit trails required by financial regulations and enables debugging production issues by replaying events. Banks use event sourcing for account transactions, storing every state change as an immutable event, enabling regulatory compliance and forensic analysis of issues that manifest only after thousands of operations.
Example 77: CQRS with F# - Command Query Separation
CQRS separates read (query) and write (command) models, optimizing each independently.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC
graph TD
A[Command]:::blue --> B[Write Model]:::orange
B --> C[Event Store]:::teal
C --> D[Read Model]:::purple
E[Query]:::blue --> D
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:#0173B2,stroke:#000,color:#fff
Code:
// Example 77: CQRS with F# - Command Query Separation
// Commands (write operations)
type Command =
| CreateOrder of orderId: string * customerId: string * items: string list
| AddItem of orderId: string * item: string
| CancelOrder of orderId: string
// Events (facts about what happened)
type OrderEvent =
| OrderCreated of orderId: string * customerId: string * items: string list
| ItemAdded of orderId: string * item: string
| OrderCancelled of orderId: string
// Write model (command handler)
let handleCommand command =
// => Command -> Event
// => Validates and produces events
match command with
| CreateOrder (id, customerId, items) ->
Ok [OrderCreated (id, customerId, items)]
| AddItem (id, item) ->
Ok [ItemAdded (id, item)]
| CancelOrder id ->
Ok [OrderCancelled id]
// Read model (optimized for queries)
type OrderReadModel = {
OrderId: string
CustomerId: string
Items: string list
Status: string
}
let applyEventToReadModel (model: OrderReadModel option) event =
// => Event -> ReadModel
// => Builds denormalized read model
match event with
| OrderCreated (id, customerId, items) ->
Some { OrderId = id; CustomerId = customerId; Items = items; Status = "Active" }
| ItemAdded (id, item) ->
model |> Option.map (fun m -> { m with Items = item :: m.Items })
| OrderCancelled id ->
model |> Option.map (fun m -> { m with Status = "Cancelled" })
// Event store (simplified)
let mutable eventStore: OrderEvent list = []
let executeCommand command =
// => Execute command and store events
match handleCommand command with
| Ok events ->
eventStore <- eventStore @ events
// => Append events to store
events
| Error msg ->
printfn "Command failed: %s" msg
[]
// Query side (read model projection)
let buildReadModel events =
// => Build read model from events
events |> List.fold applyEventToReadModel None
// Execute commands
executeCommand (CreateOrder ("ORD1", "CUST1", ["Item1"; "Item2"]))
executeCommand (AddItem ("ORD1", "Item3"))
// Query read model
let readModel = buildReadModel eventStore
// => Build current read model
match readModel with
| Some model ->
printfn "Order: %s" model.OrderId
printfn "Customer: %s" model.CustomerId
printfn "Items: %A" model.Items
printfn "Status: %s" model.Status
| None ->
printfn "No order found"Key Takeaway: CQRS separates commands (writes) from queries (reads), optimizing write models for consistency and read models for query performance with denormalization.
Why It Matters: CQRS enables independent scaling of read and write workloads. E-commerce platforms handle 100x more reads than writes—CQRS allows caching aggressive read models while maintaining strict consistency in write models, achieving low query latency for product listings while ensuring order transactions remain ACID-compliant.
Example 78: Make Illegal States Unrepresentable
Domain modeling with types makes invalid states impossible to construct, eliminating validation logic.
// Example 78: Make Illegal States Unrepresentable
// BAD: Allows invalid states
type BadOrder = {
Id: string
Status: string // => Can be any string (typos possible)
ShippingAddress: string option
// => Can be None even for shipped orders
}
// GOOD: Invalid states are unrepresentable
type UnshippedOrder = { Id: string }
type ShippedOrder = { Id: string; ShippingAddress: string }
// => Shipped MUST have address
type CancelledOrder = { Id: string; Reason: string }
type OrderStatus =
| Unshipped of UnshippedOrder
| Shipped of ShippedOrder
| Cancelled of CancelledOrder
// State transitions enforce business rules
let ship (order: UnshippedOrder) (address: string) : ShippedOrder =
// => Can only ship unshipped orders
// => MUST provide address
{ Id = order.Id; ShippingAddress = address }
let cancel (order: UnshippedOrder) (reason: string) : CancelledOrder =
// => Can only cancel unshipped orders
{ Id = order.Id; Reason = reason }
// Usage
let order = Unshipped { Id = "ORD123" }
// => Order starts unshipped
let shippedOrder = match order with
| Unshipped o -> ship o "123 Main St"
// => Ship with address
| _ -> failwith "Cannot ship this order"
printfn "Shipped to: %s" shippedOrder.ShippingAddress
// COMPILE ERROR: Cannot ship already shipped order
// let reshipped = ship shippedOrder "456 Elm St"
// => Type error: ship requires UnshippedOrder
// More complex example: Email validation
type UnvalidatedEmail = UnvalidatedEmail of string
type ValidatedEmail = private ValidatedEmail of string
// => private constructor prevents direct creation
module Email =
let create (s: string) : Result<ValidatedEmail, string> =
// => Smart constructor
if s.Contains("@") then
Ok (ValidatedEmail s)
else
Error "Invalid email format"
let value (ValidatedEmail s) = s
// => Unwrap validated email
// Usage
let emailResult = Email.create "alice@example.com"
match emailResult with
| Ok email ->
printfn "Valid email: %s" (Email.value email)
| Error msg ->
printfn "Error: %s" msg
// COMPILE ERROR: Cannot create ValidatedEmail directly
// let badEmail = ValidatedEmail "notanemail"
// => Constructor is privateKey Takeaway: Design types so invalid states cannot be constructed, moving validation from runtime checks to compile-time guarantees through the type system.
Why It Matters: Making illegal states unrepresentable eliminates much validation logic and prevents bugs that slip through validation. Financial systems model trades with separate types for PendingTrade, ExecutedTrade, and SettledTrade, making it impossible to settle a pending trade or re-execute an executed trade—errors caught at compile time instead of production.
Example 79: Advanced Parser Combinators
Parser combinators compose small parsers into complex grammars using functional composition.
// Example 79: Advanced Parser Combinators
type Parser<'a> = Parser of (string -> Result<'a * string, string>)
// => Parser is function: input -> Result
let run (Parser p) input = p input
// Primitive parsers
let pchar c = Parser(fun input ->
// => Parse single character
if String.length input > 0 && input.[0] = c then
Ok (c, input.[1..])
// => Success: return char and remaining input
else
Error (sprintf "Expected '%c'" c)
)
let (<|>) (Parser p1) (Parser p2) = Parser(fun input ->
// => Choice combinator: try p1, then p2
match p1 input with
| Ok result -> Ok result
| Error _ -> p2 input
)
let (>>=) (Parser p) f = Parser(fun input ->
// => Bind combinator: sequence parsers
match p input with
| Ok (value, remaining) ->
let (Parser p2) = f value
p2 remaining
| Error msg -> Error msg
)
let preturn value = Parser(fun input -> Ok (value, input))
// => Return combinator: always succeeds
// Parse digit
let pdigit = Parser(fun input ->
if String.length input > 0 && System.Char.IsDigit(input.[0]) then
Ok (input.[0], input.[1..])
else
Error "Expected digit"
)
// Many combinator: 0 or more
let rec many (Parser p) = Parser(fun input ->
match p input with
| Ok (value, remaining) ->
match run (many (Parser p)) remaining with
| Ok (values, final) -> Ok (value :: values, final)
| Error _ -> Ok ([value], remaining)
| Error _ -> Ok ([], input)
)
// Parse integer
let pint = many pdigit >>= (fun digits ->
let s = System.String(List.toArray digits)
preturn (int s)
)
// Test parsers
match run (pchar 'a') "abc" with
| Ok (c, remaining) ->
printfn "Parsed: '%c', Remaining: '%s'" c remaining
// => Outputs: Parsed: 'a', Remaining: 'bc'
| Error msg ->
printfn "Error: %s" msg
match run pint "123abc" with
| Ok (value, remaining) ->
printfn "Parsed int: %d, Remaining: '%s'" value remaining
// => Outputs: Parsed int: 123, Remaining: 'abc'
| Error msg ->
printfn "Error: %s" msgKey Takeaway: Parser combinators compose primitive parsers using functional operators, building complex grammars from simple building blocks without parser generators.
Why It Matters: Parser combinators enable embedded DSLs for parsing without external tools. Compilers use parser combinators to parse programming languages entirely in F#, achieving faster development than yacc/bison while maintaining type safety and composability for parser evolution.
Example 80: Metaprogramming with Code Generation
F# type providers and quotations enable compile-time code generation from external schemas.
// Example 80: Metaprogramming with Code Generation
open FSharp.Quotations
// Generate function from quotation
let generateAdder x = <@ fun y -> y + x @>
// => Quotation capturing x
// => Generates function at compile time
let addFive = generateAdder 5
// => addFive is quotation: fun y -> y + 5
// Evaluate quotation (simplified)
let evalQuotation (expr: Expr<'a>) : 'a =
// => Evaluate quotation to value
FSharp.Linq.RuntimeHelpers.LeafExpressionConverter.EvaluateQuotation(expr)
:?> 'a
let adder = evalQuotation addFive
// => adder is function: int -> int
let result = adder 10 // => result is 15
printfn "Result: %d" result
// Code generation pattern
type GeneratedCode = {
PropertyName: string
PropertyType: string
}
let generateProperty prop =
// => Generate property code string
sprintf "member this.%s : %s" prop.PropertyName prop.PropertyType
let properties = [
{ PropertyName = "Id"; PropertyType = "int" }
{ PropertyName = "Name"; PropertyType = "string" }
]
let generatedCode = properties |> List.map generateProperty
// => Generate code strings
for code in generatedCode do
printfn "%s" code
// => Outputs:
// => member this.Id : int
// => member this.Name : string
// Quotation manipulation
let replaceValue (expr: Expr) (oldVal: int) (newVal: int) =
// => Transform quotation
let rec replace e =
match e with
| Quotations.Patterns.Value (v, t) when v :? int && (v :?> int) = oldVal ->
Expr.Value(newVal, t)
| Quotations.Patterns.Call (target, method, args) ->
Expr.Call(target, method, List.map replace args)
| _ -> e
replace expr
let original = <@ 1 + 2 + 3 @>
// => Original quotation
let modified = replaceValue original 2 20
// => Replace 2 with 20
// => Result: 1 + 20 + 3 = 24
printfn "Original: %A" original
printfn "Modified: %A" modifiedKey Takeaway: Metaprogramming with quotations and code generation enables compile-time code synthesis from schemas, reducing boilerplate and ensuring generated code type-checks.
Why It Matters: Type providers generate type-safe database access code from schemas at compile time. SQL type providers read database schemas and generate F# types with IntelliSense support, eliminating most hand-written data access code while catching schema mismatches at compile time instead of runtime.
Example 81: C# Interop - Consuming and Exposing APIs
F# seamlessly interoperates with C# libraries while exposing F#-friendly APIs to C# consumers.
// Example 81: C# Interop - Consuming and Exposing APIs
open System.Collections.Generic
// Consuming C# APIs
let dict = Dictionary<string, int>()
// => C# Dictionary from BCL
dict.Add("apple", 1) // => Mutable C# API
dict.Add("banana", 2)
printfn "Apple count: %d" dict.["apple"]
// => Outputs: Apple count: 1
// Convert to F# map
let fsharpMap = dict |> Seq.map (|KeyValue|) |> Map.ofSeq
// => Convert to immutable F# Map
// => |KeyValue| is active pattern
printfn "F# Map: %A" fsharpMap
// Exposing F# code to C#
type FSharpLibrary() =
// => Class visible to C#
member _.Add(x: int, y: int) : int =
// => Simple method
x + y
member _.ProcessList(items: int list) : int =
// => F# list parameter (C# sees IEnumerable)
items |> List.sum
member _.GetOption(value: int option) : System.Nullable<int> =
// => Option -> Nullable for C#
match value with
| Some v -> System.Nullable<int>(v)
| None -> System.Nullable<int>()
static member CreatePerson(name: string, age: int) =
// => Factory method for C#
{ Name = name; Age = age }
type Person = { Name: string; Age: int }
// Attribute for C# property access
[<CLIMutable>]
type CLIPerson = {
Name: string
Age: int
} // => [<CLIMutable>] adds default constructor
// => Enables C# object initializer syntax
// Extension methods for C#
[<System.Runtime.CompilerServices.Extension>]
module StringExtensions =
[<System.Runtime.CompilerServices.Extension>]
let ToUpperInvariant (s: string) =
// => Extension method visible to C#
s.ToUpperInvariant()
let lib = FSharpLibrary()
printfn "Sum: %d" (lib.Add(5, 10))
printfn "List sum: %d" (lib.ProcessList([1; 2; 3; 4; 5]))
let someValue = Some 42
printfn "Nullable: %A" (lib.GetOption(someValue))Key Takeaway: F# interoperates seamlessly with C# using attribute annotations, converting F# types (options, lists) to C#-friendly equivalents (Nullable, IEnumerable) at API boundaries.
Why It Matters: Seamless C# interop enables incremental F# adoption in existing .NET codebases. Teams introduce F# for domain modeling and business logic (exploiting algebraic types and immutability) while exposing C#-friendly APIs, reducing bugs through F# safety without rewriting entire applications.
Example 82: Performance Optimization - Struct vs Class
F# supports both reference types (classes) and value types (structs) for performance-critical scenarios.
// Example 82: Performance Optimization - Struct vs Class
// Reference type (class) - heap allocated
type PointClass = { X: float; Y: float }
// Value type (struct) - stack allocated
[<Struct>]
type PointStruct = { X: float; Y: float }
// Performance comparison (allocation)
let classPoints = [| for i in 1 .. 1000000 -> { X = float i; Y = float i } |]
// => 1M PointClass allocations on heap
// => Triggers garbage collection
let structPoints = [| for i in 1 .. 1000000 -> { X = float i; Y = float i } |]
// => 1M PointStruct values on stack/inline
// => No GC pressure
printfn "Class points: %d" classPoints.Length
printfn "Struct points: %d" structPoints.Length
// Struct with interface (boxed when cast to interface)
[<Struct>]
type Counter = {
mutable Count: int // => Mutable field allowed in struct
} with
interface System.IComparable with
member this.CompareTo(obj) =
match obj with
| :? Counter as other -> this.Count.CompareTo(other.Count)
| _ -> 1
let mutable counter = { Count = 0 }
// => Struct value
counter.Count <- 10 // => Mutation (structs are mutable!)
printfn "Counter: %d" counter.Count
// When to use struct vs class
// Struct: Small (<16 bytes), frequently allocated, numeric-like
// Class: Large, infrequently allocated, reference semantics needed
// Inline functions for performance
let inline add x y = x + y
// => inline removes function call overhead
// => Code inlined at call site
let sum = add 10 20 // => No function call, direct addition
printfn "Sum: %d" sumKey Takeaway: Structs reduce GC pressure through stack allocation and inlining, improving performance for small, frequently-allocated types at the cost of value semantics.
Why It Matters: Struct optimization eliminates 90% of garbage collection pauses in numerical computing. Game engines use structs for vectors and points, processing millions per frame with zero GC overhead, achieving high frame rates triggering GC every frame.
Example 83: Profiling and Benchmarking
F# supports profiling tools for identifying performance bottlenecks and memory issues.
// Example 83: Profiling and Benchmarking
open System.Diagnostics
// Simple benchmarking
let benchmark name f =
// => Benchmark function execution
let sw = Stopwatch.StartNew()
let result = f()
sw.Stop()
printfn "%s: %dms" name sw.ElapsedMilliseconds
result
// Compare list vs sequence performance
let listSum = benchmark "List.sum" (fun () ->
[1..1000000] |> List.sum
) // => Eager evaluation
let seqSum = benchmark "Seq.sum" (fun () ->
seq { 1..1000000 } |> Seq.sum
) // => Lazy evaluation
printfn "List sum: %d" listSum
printfn "Seq sum: %d" seqSum
// Memory profiling pattern
let measureMemory name f =
// => Measure GC allocations
GC.Collect() // => Force GC before measurement
GC.WaitForPendingFinalizers()
let before = GC.GetTotalMemory(true)
let result = f()
let after = GC.GetTotalMemory(true)
printfn "%s allocated: %d bytes" name (after - before)
result
let listAlloc = measureMemory "List allocation" (fun () ->
[1..10000] |> List.map (fun x -> x * 2)
)
let seqAlloc = measureMemory "Seq allocation" (fun () ->
seq { 1..10000 } |> Seq.map (fun x -> x * 2) |> Seq.toList
)
// Performance-critical patterns
// 1. Avoid boxing/unboxing
let boxedSum = [1..100] |> List.map box |> List.sumBy (fun x -> x :?> int)
// => Boxing creates heap allocations
let directSum = [1..100] |> List.sum
// => No boxing, direct primitive operations
// 2. Use Array for hot paths
let arraySum = [|1..1000000|] |> Array.sum
// => Array is faster than List for large data
// 3. Memoization for expensive computations
let memoize f =
// => Cache function results
let cache = System.Collections.Generic.Dictionary<_, _>()
fun x ->
match cache.TryGetValue(x) with
| true, result -> result
| false, _ ->
let result = f x
cache.[x] <- result
result
let rec fib n =
if n <= 1 then n
else fib (n - 1) + fib (n - 2)
let fibMemo = memoize fib
let slow = benchmark "Fibonacci recursive" (fun () -> fib 30)
let fast = benchmark "Fibonacci memoized" (fun () -> fibMemo 30)
printfn "Slow fib: %d" slow
printfn "Fast fib: %d" fastKey Takeaway: Profile before optimizing using Stopwatch for timing and GC.GetTotalMemory for allocation tracking. Common optimizations: avoid boxing, use arrays for hot paths, memoize expensive computations.
Why It Matters: Profiling reveals actual bottlenecks vs assumed ones. Production systems often discover performance bottlenecks in unexpected places—profiling prevents premature optimization while identifying real issues like unnecessary allocations causing GC pauses.
Example 84: Concurrent Collections and Lock-Free Data Structures
F# uses .NET concurrent collections for thread-safe operations without explicit locks.
// Example 84: Concurrent Collections and Lock-Free Data Structures
open System.Collections.Concurrent
open System.Threading.Tasks
// ConcurrentBag: unordered, thread-safe collection
let bag = ConcurrentBag<int>()
Parallel.For(0, 100, fun i ->
// => 100 parallel tasks
bag.Add(i) // => Thread-safe add
) |> ignore
printfn "Bag count: %d" bag.Count
// => Outputs: Bag count: 100
// ConcurrentDictionary: thread-safe key-value store
let dict = ConcurrentDictionary<string, int>()
let increment key =
// => Atomic increment
dict.AddOrUpdate(key, 1, fun k v -> v + 1)
Parallel.For(0, 1000, fun i ->
increment "counter" |> ignore
) |> ignore
printfn "Counter value: %d" dict.["counter"]
// => Outputs: Counter value: 1000
// => All 1000 increments successful (no race conditions)
// ConcurrentQueue: thread-safe FIFO queue
let queue = ConcurrentQueue<string>()
Parallel.For(0, 10, fun i ->
queue.Enqueue(sprintf "Item %d" i)
) |> ignore
let mutable item = ""
while queue.TryDequeue(&item) do
printfn "Dequeued: %s" item
// Lock-free counter using Interlocked
open System.Threading
let mutable counter = 0
Parallel.For(0, 10000, fun i ->
Interlocked.Increment(&counter) |> ignore
// => Atomic increment (lock-free)
) |> ignore
printfn "Lock-free counter: %d" counter
// => Outputs: Lock-free counter: 10000
// Compare with unsafe increment
let mutable unsafeCounter = 0
Parallel.For(0, 10000, fun i ->
unsafeCounter <- unsafeCounter + 1
// => RACE CONDITION: read-modify-write not atomic
) |> ignore
printfn "Unsafe counter: %d" unsafeCounter
// => Outputs: Unsafe counter: < 10000
// => Lost updates due to race conditionsKey Takeaway: Use ConcurrentBag, ConcurrentDictionary, and ConcurrentQueue for thread-safe collections. Use Interlocked for lock-free atomic operations on primitives.
Why It Matters: Concurrent collections eliminate explicit locking complexity while maintaining thread safety. Web services use ConcurrentDictionary for request caches shared across threads, achieving significant throughput improvements over lock-based dictionaries without deadlock risk.
Example 85: Advanced Async Patterns - Cancellation and Timeouts
Async workflows support cancellation tokens and timeouts for robust async programming.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC
graph TD
A[Start Async]:::blue --> B{Cancelled?}:::orange
B -->|No| C[Continue Work]:::teal
B -->|Yes| D[Clean Cancel]:::purple
C --> E{Timeout?}:::orange
E -->|No| F[Complete]:::teal
E -->|Yes| D
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:#DE8F05,stroke:#000,color:#fff
style F fill:#029E73,stroke:#000,color:#fff
Code:
// Example 85: Advanced Async Patterns - Cancellation and Timeouts
open System
open System.Threading
// Cancellable async operation
let longRunningOperation (ct: CancellationToken) = async {
// => Accept cancellation token
for i in 1 .. 10 do
do! Async.Sleep 500
// => Simulate work
ct.ThrowIfCancellationRequested()
// => Check for cancellation
printfn "Step %d completed" i
}
// Execute with cancellation
let cts = new CancellationTokenSource()
let task = Async.Start(longRunningOperation cts.Token)
// => Start async with cancellation token
Thread.Sleep(2500) // => Wait 2.5 seconds
cts.Cancel() // => Cancel operation
// => Operation stops cleanly
Thread.Sleep(500)
printfn "Operation cancelled"
// Timeout pattern
let withTimeout (milliseconds: int) (operation: Async<'a>) : Async<'a option> =
async {
let! result = Async.Choice [
// => Race multiple async operations
async {
let! value = operation
return Some value
}
async {
do! Async.Sleep milliseconds
return None // => Timeout result
}
]
return result
}
let slowOperation = async {
do! Async.Sleep 3000 // => 3 second operation
return "Completed"
}
let fastOperation = async {
do! Async.Sleep 500 // => 500ms operation
return "Completed"
}
// Timeout after 1 second
let slowResult = withTimeout 1000 slowOperation |> Async.RunSynchronously
// => slowResult is None (timed out)
let fastResult = withTimeout 1000 fastOperation |> Async.RunSynchronously
// => fastResult is Some "Completed"
match slowResult with
| Some value -> printfn "Slow: %s" value
| None -> printfn "Slow operation timed out"
match fastResult with
| Some value -> printfn "Fast: %s" value
| None -> printfn "Fast operation timed out"
// Parallel with timeout
let parallelWithTimeout operations timeout = async {
let! results = operations
|> List.map (withTimeout timeout)
|> Async.Parallel
// => Run all with timeout in parallel
return results
}
let ops = [
async { do! Async.Sleep 500; return 1 }
async { do! Async.Sleep 2000; return 2 } // => Will timeout
async { do! Async.Sleep 300; return 3 }
]
let results = parallelWithTimeout ops 1000 |> Async.RunSynchronously
for result in results do
match result with
| Some value -> printfn "Result: %d" value
| None -> printfn "Result: Timed out"Key Takeaway: Async workflows integrate cancellation tokens for cooperative cancellation and support timeout patterns through Async.Choice, enabling robust async programming with graceful degradation.
Why It Matters: Cancellation prevents resource leaks when requests are abandoned. Microservices cancel downstream requests when clients disconnect, preventing wasted computation and database queries for results no one will receive, improving throughput during traffic spikes by freeing resources immediately.
Summary
These 25 advanced examples (Examples 61-85) cover 75-95% of F#’s features, building on beginner and intermediate fundamentals with expert patterns:
Concurrency & Parallelism:
- MailboxProcessor agents for actor-based concurrency
- Async.Parallel for parallel I/O
- Concurrent collections and lock-free operations
Type System Mastery:
- SRTP for compile-time polymorphism
- Phantom types for type-state patterns
- Units of measure for dimensional analysis
- GADTs emulation for type-safe evaluation
Metaprogramming:
- Quotations for code as data
- Reflection for runtime type inspection
- Custom computation expression builders
- Code generation and type providers
Domain Modeling:
- Make illegal states unrepresentable
- Event sourcing and CQRS patterns
- Functional dependency injection
Performance & Interop:
- Struct vs class optimization
- Profiling and benchmarking
- C# interoperability
- Parser combinators
Advanced Patterns:
- Parameterized active patterns
- Type extensions
- Custom operators
- Async cancellation and timeouts
You now have comprehensive F# knowledge spanning 85 examples from Hello World to advanced functional architecture, covering 95% of language features for production F# development.