โ๏ธ F# Deep Foundations
๐ฏ F# Design Philosophy & Execution Model
F# (Microsoft Research, 2005, Don Syme) is a strongly-typed, functional-first language that runs on .NET (CLR). It combines functional, imperative, and object-oriented paradigms. F# emphasizes immutability by default, type inference, and pattern matching. It compiles to efficient .NET IL (Intermediate Language) and interoperates seamlessly with C# and VB.NET libraries.
Deep Explanation
F# is both functional and practical: It encourages a functional style with immutable data and pure functions while providing full access to the .NET ecosystem. The REPL (F# Interactive) enables rapid prototyping. Unlike C#, F# uses automatic generalization to infer the most generic types.
Why it matters: F# reduces bugs through immutability, increases productivity with concise syntax, and handles complex data transformations elegantly. It's widely used in financial trading, data science (with Deedle), and web development (Fable, SAFE Stack).
// F# Interactive (REPL) - try it!
printfn "Welcome to F#!"
// Type inference in action
let meaningOfLife = 42 // int inferred
let pi = 3.14159 // float inferred
let message = "Hello" // string inferred
// Function definition with type inference
let add x y = x + y // 'a -> 'a -> 'a (generic)
printfn "6 + 7 = %d" (add 6 7)
// Inspect types in F# Interactive
// > typeof;;
// > add.GetType();; ๐ Immutability & Binding
๐ฏ let Bindings & Immutable Values
let creates immutable values. let mutable allows mutation (discouraged). F# prioritizes referential transparency โ expressions can be replaced with their values without changing program behavior.
Immutability = Safer Code
When you use let x = 5, x cannot be reassigned. This eliminates entire classes of bugs related to unexpected state changes. For data that needs to "change," F# encourages creating new immutable copies rather than mutating existing ones.
Shadowing allows reusing names without mutation: let x = 10; let x = x + 5 creates a new binding.
// Immutable binding
let name = "F# Developer"
// name <- "New Name" // ERROR - cannot reassign
// Shadowing (not mutation)
let count = 10
let count = count + 5 // New binding, old count shadowed
printfn "%d" count // 15
// Mutable (rarely needed)
let mutable counter = 0
counter <- counter + 1 // Allowed but use sparingly
// Tuples are immutable
let coordinates = (10, 20)
let swapped = (snd coordinates, fst coordinates)๐ฏ First-Class Functions & Pipelines
๐ฏ Functions as Values
Functions are first-class โ they can be passed as arguments, returned from other functions, and composed. The pipeline operator (|>) chains functions elegantly.
Function Composition & Pipelining
The pipeline operator |> threads a value through a series of functions: data |> func1 |> func2 |> func3. This is functionally equivalent to func3 (func2 (func1 data)) but much more readable. Partial application allows you to "pre-fill" some arguments of a function.
// Define a function
let square x = x * x
let addOne x = x + 1
// Pipeline (left-to-right)
let result = 5 |> addOne |> square
printfn "%d" result // (5+1)^2 = 36
// Composition (mathematical style)
let addOneThenSquare = addOne >> square
printfn "%d" (addOneThenSquare 5)
// Partial application
let add x y = x + y
let addFive = add 5
printfn "%d" (addFive 10) // 15
// Higher-order function
let applyTwice f x = f (f x)
printfn "%d" (applyTwice addOne 5) // 7๐ Pattern Matching Mastery
๐ฏ match Expressions & Deconstruction
Pattern matching is F's superpower. It deconstructs data, handles cases exhaustively, and can match on shapes, values, and types.
Exhaustive Pattern Matching
Unlike switch statements in other languages, F#'s match is exhaustive โ the compiler warns if you miss a case. Patterns can include: constants, variables, wildcards (_), cons patterns (head :: tail), and guards (when conditions).
// Simple pattern matching
let describeNumber x =
match x with
| 0 -> "Zero"
| 1 -> "One"
| 2 -> "Two"
| _ -> "Many" // underscore = wildcard
printfn "%s" (describeNumber 42)
// Pattern matching with guards
let classifyAge age =
match age with
| a when a < 18 -> "Minor"
| a when a < 65 -> "Adult"
| _ -> "Senior"
// List deconstruction (cons pattern)
let rec sumList lst =
match lst with
| [] -> 0
| head :: tail -> head + sumList tail
printfn "%d" (sumList [1;2;3;4;5])๐ Collection Modules (List, Seq, Array)
๐ฏ List, Seq, Array: When to Use Which
List (immutable singly-linked) โ O(1) prepend, Array (mutable fixed-size) โ O(1) index, Seq (lazy IEnumerable) โ streaming data.
Collection Pipelines
All three modules provide map, filter, fold, collect, and more. Seq is lazy โ operations are deferred until enumeration. List is eager and fully immutable. Array is mutable and offers maximum performance for numerical computations.
// List (immutable, eager)
let numbers = [1..10]
let evens = numbers |> List.filter (fun x -> x % 2 = 0)
let squares = numbers |> List.map (fun x -> x * x)
// Lazy Seq (deferred execution)
let lazySquares = numbers |> Seq.map (fun x ->
printfn "Computing %d" x
x * x
)
let firstTwo = lazySquares |> Seq.take 2 |> Seq.toList // Only computes 1 and 2
// Array (mutable, fast)
let arr = [|1..1000000|]
let sum = arr |> Array.sum
// List comprehension
let pythagorean = [ for x in 1..10 do for y in x..10 do if x*x + y*y = 100 then yield (x,y) ]๐ท๏ธ Discriminated Unions
๐ฏ One-of-Many Types
Discriminated Unions (DUs) represent a choice between different cases โ like enums but each case can carry data.
Modeling Domain with DUs
DUs are perfect for domain modeling: type Result<'TSuccess,'TFailure>, type Option<'T> (Some/None), type Either<'L,'R>. The compiler ensures exhaustive handling via pattern matching.
// Define a Discriminated Union
type PaymentMethod =
| Cash
| CreditCard of cardNumber: string * expiry: string
| PayPal of email: string
// Use with pattern matching
let processPayment method amount =
match method with
| Cash -> printfn "Paid $%f in cash" amount
| CreditCard(num, exp) -> printfn "Paid $%f using card %s (exp %s)" amount num exp
| PayPal(email) -> printfn "Paid $%f via PayPal (%s)" amount email
// Option type is a DU: Some('T) | None
let divide x y =
if y = 0 then None else Some(x / y)
match divide 10 2 with
| Some result -> printfn "Result: %d" result
| None -> printfn "Division by zero"๐ Records & Anonymous Records
๐ฏ Immutable Data Aggregates
Records are immutable named tuples with structural equality โ ideal for data transfer objects (DTOs).
Copy-and-Update Expressions
Records support with expressions for creating modified copies: { person with Name = "New Name" }. Records automatically implement IComparable and structural equality. Anonymous records are lightweight ad-hoc data containers.
// Define a record
type Person = {
Name: string
Age: int
Email: string
}
// Create record
let alice = { Name = "Alice"; Age = 30; Email = "alice@example.com" }
// Copy-and-update (immutable)
let olderAlice = { alice with Age = 31 }
// Anonymous records (F# 4.6+)
let point = {| X = 10; Y = 20 |}
printfn "X=%d, Y=%d" point.X point.Y
// Pattern matching on records
let describePerson person =
match person with
| { Name = name; Age = age } when age < 18 -> $"{name} is a minor"
| { Name = name } -> $"{name} is an adult"โ๏ธ Computation Expressions (Monads)
๐ฏ Builder Pattern for Monadic Code
Computation expressions (CEs) are F#'s syntactic sugar for monads โ enabling async workflows, option workflows, and custom DSLs using let! and return.
Option, Async, Seq Workflows
async { ... } for non-blocking I/O, seq { ... } for lazy sequences, maybe { ... } for optional chaining. The let! binder unwraps monadic values while return wraps them.
// Option workflow (Maybe monad)
type MaybeBuilder() =
member _.Bind(x, f) = Option.bind f x
member _.Return(x) = Some x
member _.ReturnFrom(x) = x
let maybe = MaybeBuilder()
let safeDivide x y = if y = 0 then None else Some(x / y)
let complexOperation =
maybe {
let! a = safeDivide 10 2 // Some 5
let! b = safeDivide a 0 // None - short-circuits
return a + b
}
printfn "%A" complexOperation // None
// Async workflow
let asyncExample = async {
do! Async.Sleep(1000)
return "Done"
}
Async.RunSynchronously asyncExample |> printfn "%s"โฉ Async & Task Programming
๐ฏ Async vs Task vs TaskBuilder
F# provides Async<'T> (cancellable, composable) and interop with .NET Task<'T>.
Non-blocking I/O & Parallelism
Async.Parallel runs multiple async computations concurrently. TaskBuilder.fs (community library) enables task { } syntax for direct C# interop. Use Async.StartAsTask to convert to Task for .NET compatibility.
open System.Net.Http
let fetchUrl url = async {
use client = new HttpClient()
let! response = client.GetStringAsync(url) |> Async.AwaitTask
return response.Length
}
// Parallel execution
let urls = ["https://fsharp.org"; "https://dot.net"]
let fetchAll = urls |> List.map fetchUrl |> Async.Parallel
let results = fetchAll |> Async.RunSynchronously
printfn "%A" results
// Cancellation
let cts = new System.Threading.CancellationTokenSource()
Async.Start(fetchUrl "https://example.com", cts.Token)
cts.Cancel()๐ Units of Measure
๐ฏ Compile-time Unit Checking
F# has built-in support for units of measure โ preventing unit conversion bugs at compile time without runtime overhead.
Dimensional Analysis
Define units with [<Measure>] attribute. The compiler tracks units and prevents adding meters to seconds. Units are erased at runtime โ no performance cost.
[] type m // meters
[] type s // seconds
[] type kg // kilograms
[] type N // newtons
let distance = 100.0
let time = 9.58
let speed = distance / time // float
let mass = 70.0
let force = mass * speed / time // float = N
// Built-in SI units are available
open FSharp.Data.UnitSystems.SI.UnitSymbols
let d = 10.0
let t = 5.0
let v = d / t // m/s
// Unit conversion
let km = 5.0
let meters = km * 1000.0 ๐ฎ Type Providers
๐ฏ Generated Types from External Sources
Type providers generate types at compile time based on external schemas โ CSV, JSON, SQL, even WSDL services โ without code generation files.
F# Unique Killer Feature
Unlike code generators, type providers integrate seamlessly into the F# tooling. FSharp.Data provides CSV, JSON, HTML, and XML providers. SqlCommandProvider validates SQL at compile time.
// Install: dotnet add package FSharp.Data
open FSharp.Data
// JSON Type Provider
type GitHubUser = JsonProvider<"https://api.github.com/users/dotnet">
let user = GitHubUser.GetSample()
printfn "User: %s, Repos: %d" user.Login user.PublicRepos
// CSV Type Provider
type Stocks = CsvProvider<"SampleData/Stocks.csv">
let data = Stocks.GetSample()
for row in data.Rows do
printfn "%A %f" row.Date row.Close
// SQL Type Provider (FSharp.Data.SqlClient)
// type SelectCmd = SqlCommandProvider<"
// SELECT * FROM Products WHERE CategoryId = @catId
// ", ConnectionString>
// use cmd = new SelectCmd()
// let products = cmd.Execute(catId = 5)๐ญ Active Patterns
๐ฏ Custom Pattern Matching
Active patterns let you define custom ways to partition and match data using (|PatternName|) syntax.
Decomposing Any Data
Active patterns extend pattern matching beyond built-in types. Partial active patterns ((|_|)) can fail. Multi-case active patterns partition a domain.
// Single-case active pattern (for wrapping)
let (|EmailAddress|) email = email.Split('@').[1]
let getDomain = function
| EmailAddress domain -> domain
printfn "%s" (getDomain "user@example.com") // "example.com"
// Partial active pattern (may fail)
let (|Even|Odd|) n = if n % 2 = 0 then Even else Odd
let describe = function
| Even -> "Even"
| Odd -> "Odd"
// Parameterized active pattern
let (|DivisibleBy|_|) divisor n =
if n % divisor = 0 then Some DivisibleBy else None
let describeDivisible n =
match n with
| DivisibleBy 2 -> "Divisible by 2"
| DivisibleBy 3 -> "Divisible by 3"
| _ -> "Not divisible by 2 or 3"๐ .NET Interop & OOP
๐ฏ Seamless .NET Integration
F# can consume any C#/VB.NET library and expose F# types to C#. It supports classes, interfaces, and inheritance for OOP interop.
Best of Both Worlds
Use type MyClass() = class ... end for standard OOP. F# implements IDisposable via use keyword. interface and abstract members work with .NET conventions. [<CLIMutable>] makes records compatible with C# serialization.
// Call C# libraries easily
open System.Collections.Generic
let dict = Dictionary()
dict.Add("one", 1)
// F# class for C# interop
[] // For serialization compatibility
type Product = {
Id: int
Name: string
Price: decimal
}
// Interface implementation
type Counter() =
interface System.IDisposable with
member _.Dispose() = printfn "Cleaning up"
member _.Increment(x) = x + 1
// Use disposable with 'use' keyword
use file = System.IO.File.CreateText("test.txt")
file.WriteLine("Hello F#") ๐ Cross-Platform (Fable, .NET)
๐ฏ Fable, .NET MAUI, Blazor
F# runs on .NET Core (Windows, Linux, macOS) and compiles to JavaScript via Fable or WebAssembly via Bolero.
Write Once, Run Anywhere
Fable compiles F# to JavaScript for React/Vue frontends. SAFE Stack (Saturn, Azure, Fable, Elmish) provides full-stack F#. Bolero targets WebAssembly with Blazor. .NET MAUI supports mobile (iOS/Android) and desktop.
// Fable: F# -> JavaScript
// Elmish architecture (Model-View-Update)
type Model = { Count: int }
type Msg =
| Increment
| Decrement
let update msg model =
match msg with
| Increment -> { model with Count = model.Count + 1 }
| Decrement -> { model with Count = model.Count - 1 }
// FAKE (F# Make) build script
Target.create "Build" (fun _ ->
DotNet.build id "MyProject.fsproj"
)
Target.runOrDefault "Build"
// .NET 8 across platforms
// dotnet new console -lang F#
// dotnet run (works on Windows, Mac, Linux)