Claude-skill-registry lang-fsharp-dev
Foundational F# patterns covering functional-first programming, type providers, computation expressions, and domain modeling. Use when writing F# code, understanding functional patterns, working with type providers, or building .NET applications with F#. This is the entry point for F# development.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/lang-fsharp-dev" ~/.claude/skills/majiayu000-claude-skill-registry-lang-fsharp-dev && rm -rf "$T"
skills/data/lang-fsharp-dev/SKILL.mdF# Fundamentals
Foundational F# patterns and core language features. This skill serves as both a reference for common patterns and guidance for functional-first .NET development.
Overview
┌─────────────────────────────────────────────────────────────────┐ │ F# Skill Hierarchy │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌───────────────────┐ │ │ │ lang-fsharp-dev │ ◄── You are here │ │ │ (foundation) │ │ │ └─────────┬─────────┘ │ │ │ │ │ ┌────────────┬───────────┼───────────┬────────────┐ │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ │ │ ┌────────┐ ┌──────────┐ ┌────────┐ ┌─────────┐ ┌──────────┐ │ │ │ type │ │ domain │ │ async │ │ testing │ │ web-api │ │ │ │providers│ │ modeling │ │ -dev │ │ -dev │ │ -dev │ │ │ └────────┘ └──────────┘ └────────┘ └─────────┘ └──────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘
This skill covers:
- Functional-first programming patterns
- Type system (records, discriminated unions, options)
- Pattern matching and active patterns
- Computation expressions basics
- Type providers fundamentals
- Domain modeling with types
- Interop with C# and .NET
This skill does NOT cover (see specialized skills):
- Advanced type providers usage
- Domain-driven design patterns
- Async/Task programming deep dive
- Testing frameworks (Expecto, xUnit, FsUnit)
- Web development (Giraffe, Saturn, Falco)
Quick Reference
| Task | Pattern |
|---|---|
| Define record | |
| Define union | |
| Pattern match | |
| Define function | |
| Pipe operator | |
| Composition | |
| List comprehension | |
| Async computation | |
Core Types
Records
// Basic record type Person = { FirstName: string LastName: string Age: int } // Creating instances let person = { FirstName = "John" LastName = "Doe" Age = 30 } // Copy-and-update let olderPerson = { person with Age = 31 } // Pattern matching on records let getFullName person = match person with | { FirstName = f; LastName = l } -> $"{f} {l}" // Shorter: destructuring let getFullName' { FirstName = f; LastName = l } = $"{f} {l}"
Discriminated Unions
// Simple union type PaymentMethod = | Cash | CreditCard of cardNumber: string | DebitCard of cardNumber: string * pin: int // Using unions let processPayment method = match method with | Cash -> "Processing cash payment" | CreditCard cardNumber -> $"Processing credit card {cardNumber}" | DebitCard (cardNumber, _) -> $"Processing debit card {cardNumber}" // Option type (built-in union) type Option<'T> = | Some of 'T | None // Result type (built-in union) type Result<'T,'E> = | Ok of 'T | Error of 'E // Using Option let findPerson id = if id = 1 then Some { FirstName = "John"; LastName = "Doe"; Age = 30 } else None // Using Result let divide x y = if y = 0 then Error "Division by zero" else Ok (x / y)
Single-Case Unions (Type Safety)
// Wrap primitives for type safety type EmailAddress = EmailAddress of string type CustomerId = CustomerId of int let sendEmail (EmailAddress email) message = printfn $"Sending to {email}: {message}" let getCustomer (CustomerId id) = // Can't accidentally pass wrong ID type printfn $"Fetching customer {id}" // Usage prevents type confusion let email = EmailAddress "test@example.com" let customerId = CustomerId 123 sendEmail email "Hello" // sendEmail customerId "Hello" // Compile error!
Pattern Matching
Basic Matching
// Match on values let describe x = match x with | 0 -> "zero" | 1 -> "one" | 2 -> "two" | n when n > 0 -> "positive" | _ -> "negative" // Match on types let processValue (value: obj) = match value with | :? string as s -> $"String: {s}" | :? int as i -> $"Int: {i}" | _ -> "Unknown type" // Match on tuples let point = (3, 4) match point with | (0, 0) -> "origin" | (x, 0) -> $"on x-axis at {x}" | (0, y) -> $"on y-axis at {y}" | (x, y) -> $"at ({x}, {y})"
Active Patterns
// Single-case active pattern let (|Even|Odd|) n = if n % 2 = 0 then Even else Odd match 42 with | Even -> "even number" | Odd -> "odd number" // Partial active pattern let (|Integer|_|) (str: string) = match System.Int32.TryParse(str) with | true, value -> Some value | false, _ -> None match "123" with | Integer n -> $"Number: {n}" | _ -> "Not a number" // Multi-case active pattern let (|Small|Medium|Large|) n = if n < 10 then Small elif n < 100 then Medium else Large match 42 with | Small -> "small" | Medium -> "medium" | Large -> "large"
Functions
Function Basics
// Simple function let add x y = x + y // Type annotations (optional but recommended for public APIs) let add' (x: int) (y: int) : int = x + y // Anonymous function (lambda) let doubled = List.map (fun x -> x * 2) [1; 2; 3] // Recursive function let rec factorial n = if n <= 1 then 1 else n * factorial (n - 1) // Tail-recursive function (optimized) let factorial' n = let rec loop acc n = if n <= 1 then acc else loop (acc * n) (n - 1) loop 1 n
Partial Application and Currying
// All F# functions are curried by default let add x y = x + y let add5 = add 5 // Partial application add5 10 // Returns 15 // Use partial application for configurable functions let greet greeting name = $"{greeting}, {name}!" let sayHello = greet "Hello" let sayHi = greet "Hi" sayHello "Alice" // "Hello, Alice!" sayHi "Bob" // "Hi, Bob!"
Function Composition
// Forward composition (>>) let add1 x = x + 1 let double x = x * 2 let add1ThenDouble = add1 >> double add1ThenDouble 5 // Returns 12 // Backward composition (<<) let doubleThenAdd1 = add1 << double doubleThenAdd1 5 // Returns 11 // Pipe operator (|>) let result = 5 |> add1 |> double |> fun x -> x + 10 // Pipe backward (<|) let sum = (+) <| 1 + 2 // Equivalent to (+) (1 + 2)
Collections
Lists
// List literals let numbers = [1; 2; 3; 4; 5] let moreNumbers = [1..10] let evenNumbers = [2..2..10] // Cons operator (::) let newList = 0 :: numbers // [0; 1; 2; 3; 4; 5] // List comprehensions let squares = [ for i in 1..10 -> i * i ] let evens = [ for i in 1..20 do if i % 2 = 0 then yield i ] // Common list functions let doubled = List.map (fun x -> x * 2) numbers let evens' = List.filter (fun x -> x % 2 = 0) numbers let sum = List.fold (+) 0 numbers let sum' = List.sum numbers let product = List.reduce (*) numbers
Arrays
// Array literals let arr = [| 1; 2; 3; 4; 5 |] // Array comprehension let squares = [| for i in 1..10 -> i * i |] // Mutable updates (in-place) arr.[0] <- 10 // Array functions (similar to List) let doubled = Array.map (fun x -> x * 2) arr let evens = Array.filter (fun x -> x % 2 = 0) arr
Sequences (Lazy)
// Infinite sequence let naturals = Seq.initInfinite id // 0, 1, 2, 3, ... // Lazy evaluation let expensiveSeq = seq { printfn "Computing..." for i in 1..5 do printfn $"Yielding {i}" yield i * i } // Only computed when enumerated expensiveSeq |> Seq.take 3 |> Seq.toList
Map and Set
// Map (immutable dictionary) let ages = Map [ ("Alice", 30); ("Bob", 25) ] let aliceAge = ages.["Alice"] // 30 let aliceAge' = Map.tryFind "Alice" ages // Some 30 let updatedAges = Map.add "Charlie" 35 ages // Set (immutable) let set1 = Set [1; 2; 3] let set2 = Set [3; 4; 5] let union = Set.union set1 set2 // {1, 2, 3, 4, 5} let intersection = Set.intersect set1 set2 // {3}
Computation Expressions
Option Computation Expression
// Option workflow type OptionBuilder() = member _.Bind(x, f) = Option.bind f x member _.Return(x) = Some x member _.ReturnFrom(x) = x let option = OptionBuilder() let validateAge age = option { let! validAge = if age >= 0 && age <= 120 then Some age else None return validAge }
Result Computation Expression
// Result workflow type ResultBuilder() = member _.Bind(x, f) = Result.bind f x member _.Return(x) = Ok x member _.ReturnFrom(x) = x let result = ResultBuilder() let divideBy x y = if y = 0 then Error "Division by zero" else Ok (x / y) let calculate = result { let! a = divideBy 10 2 // Ok 5 let! b = divideBy 20 4 // Ok 5 let! c = divideBy a b // Ok 1 return c }
Async Computation Expression
// Async workflow (built-in) let fetchData url = async { printfn $"Fetching {url}..." do! Async.Sleep 1000 return $"Data from {url}" } let processUrls urls = async { let! results = urls |> List.map fetchData |> Async.Parallel return results |> Array.toList } // Run async computation let urls = ["url1"; "url2"; "url3"] processUrls urls |> Async.RunSynchronously
Type Providers
CSV Type Provider
open FSharp.Data // Type provider infers schema from sample type StockData = CsvProvider<"stocks.csv"> let data = StockData.Load("stocks.csv") for row in data.Rows do printfn $"{row.Date}: {row.Close}"
JSON Type Provider
open FSharp.Data // Infer from sample JSON type Weather = JsonProvider<""" { "temperature": 72, "condition": "sunny", "humidity": 65 } """> let weather = Weather.Load("weather.json") printfn $"Temperature: {weather.Temperature}°F"
SQL Type Provider
open FSharp.Data.Sql type Sql = SqlDataProvider< ConnectionString = "Server=localhost;Database=mydb", DatabaseVendor = Common.DatabaseProviderTypes.MSSQLSERVER> let ctx = Sql.GetDataContext() let customers = query { for customer in ctx.Dbo.Customers do where (customer.Age > 18) select customer }
Domain Modeling
Making Illegal States Unrepresentable
// Bad: Can represent invalid states type EmailContactBad = { EmailAddress: string option IsVerified: bool } // Problem: IsVerified can be true when EmailAddress is None // Good: Invalid states impossible type VerifiedEmail = VerifiedEmail of string type UnverifiedEmail = UnverifiedEmail of string type EmailContact = | Verified of VerifiedEmail | Unverified of UnverifiedEmail // Can only verify an unverified email let verify (UnverifiedEmail email) = // Verification logic... VerifiedEmail email
Smart Constructors
// Constrained types with validation type EmailAddress = private EmailAddress of string module EmailAddress = let create (email: string) = if email.Contains("@") then Ok (EmailAddress email) else Error "Invalid email format" let value (EmailAddress email) = email // Usage match EmailAddress.create "test@example.com" with | Ok email -> printfn $"Valid: {EmailAddress.value email}" | Error msg -> printfn $"Error: {msg}"
Units of Measure
// Define units [<Measure>] type kg [<Measure>] type m [<Measure>] type s // Type-safe calculations let distance = 100.0<m> let time = 10.0<s> let speed = distance / time // Type: float<m/s> // Prevents mixing units let mass = 50.0<kg> // let invalid = distance + mass // Compile error! // Converting units let metersToKilometers (x: float<m>) : float = float x / 1000.0
Interop with C#
Calling C# from F#
// Use C# classes naturally open System.Collections.Generic let dict = Dictionary<string, int>() dict.Add("one", 1) dict.Add("two", 2) // LINQ extension methods open System.Linq let numbers = [1..10] let evens = numbers.Where(fun x -> x % 2 = 0).ToList()
Calling F# from C#
// Design F# types for C# consumption namespace MyLibrary // Use [<CLIMutable>] for record types [<CLIMutable>] type Person = { FirstName: string LastName: string Age: int } // Use classes for OO APIs type PersonService() = member _.GetPerson(id: int) : Person option = Some { FirstName = "John"; LastName = "Doe"; Age = 30 } // Convert Option to nullable for C# member this.TryGetPerson(id: int) : Person = match this.GetPerson(id) with | Some p -> p | None -> Unchecked.defaultof<Person>
Module System
F# uses modules and namespaces to organize code. Modules are similar to static classes in C# but are more flexible and support nested modules, functions, and values.
Namespaces
// File: Domain/User.fs namespace MyApp.Domain type User = { Id: int Name: string Email: string } // Can have multiple types in same namespace type UserRepository() = member _.GetUser(id: int) : User option = None
Modules
// Top-level module (one per file) module MyApp.Utils let add x y = x + y let multiply x y = x * y // Using from another file open MyApp.Utils let result = add 5 10
Nested Modules
module MyApp.Math // Nested module module Arithmetic = let add x y = x + y let subtract x y = x - y module Geometry = let area radius = System.Math.PI * radius * radius let circumference radius = 2.0 * System.Math.PI * radius // Usage open MyApp.Math let sum = Arithmetic.add 5 10 let circle = Geometry.area 5.0
Module Attributes
// AutoOpen: automatically opens when parent is opened [<AutoOpen>] module Helpers = let inline tap f x = f x; x let inline tee f x = f x |> ignore; x // RequireQualifiedAccess: must use module name [<RequireQualifiedAccess>] module Config = let apiKey = "secret-key" let timeout = 30 // Usage: // Can't do: open Config // Must do: Config.apiKey
Open Declarations
// Open entire namespace open System.Collections.Generic // Open specific module open MyApp.Domain // Open with alias open System.Collections.Generic open SCG = System.Collections.Generic let dict = SCG.Dictionary<string, int>() // Open type (type extensions available) open type System.Math let result = PI * 2.0 // Instead of Math.PI
Signature Files (.fsi)
Signature files define the public API of a module, hiding implementation details.
// File: Domain.fsi (signature) module MyApp.Domain type User = { Id: int Name: string } val createUser: string -> User val validateUser: User -> Result<User, string> // Internal functions not exposed // File: Domain.fs (implementation) module MyApp.Domain type User = { Id: int Name: string } // Public functions (in signature) let createUser name = { Id = 0; Name = name } let validateUser user = if user.Name.Length > 0 then Ok user else Error "Invalid name" // Private helper (not in signature) let private generateId() = System.Random().Next()
Module Organization Patterns
// File-per-type pattern // User.fs namespace MyApp.Domain type User = { Id: int Name: string } module User = let create name = { Id = 0; Name = name } let setName name user = { user with Name = name } // Module-per-concept pattern module MyApp.UserManagement type User = { Id: int; Name: string } type Role = Admin | User | Guest let createUser name = { Id = 0; Name = name } let assignRole user role = (user, role)
Error Handling
F# provides powerful error handling through Option and Result types, enabling railway-oriented programming patterns that make error flows explicit and composable.
Option Type
// Option represents optional values type Option<'T> = | Some of 'T | None // Returning Option from functions let tryDivide x y = if y = 0 then None else Some (x / y) // Pattern matching match tryDivide 10 2 with | Some result -> printfn $"Result: {result}" | None -> printfn "Cannot divide by zero" // Option module functions let doubled = Option.map (fun x -> x * 2) (Some 5) // Some 10 let getOrDefault = Option.defaultValue 0 None // 0
Result Type
// Result represents success or failure with error details type Result<'T, 'TError> = | Ok of 'T | Error of 'TError // Basic usage let divide x y = if y = 0 then Error "Division by zero" else Ok (x / y) // Pattern matching match divide 10 2 with | Ok result -> printfn $"Success: {result}" | Error msg -> printfn $"Error: {msg}" // Result module functions let doubled = Result.map (fun x -> x * 2) (Ok 5) // Ok 10 let chained = Ok 10 |> Result.bind (fun x -> divide x 2) // Ok 5
Railway-Oriented Programming
Railway-oriented programming treats successful and error paths as parallel railway tracks, making composition natural.
// Validation functions that can fail let validateName name = if String.IsNullOrWhiteSpace(name) then Error "Name cannot be empty" else Ok name let validateAge age = if age >= 0 && age <= 120 then Ok age else Error "Age must be between 0 and 120" let validateEmail email = if email.Contains("@") then Ok email else Error "Invalid email format" // Composition with Result computation expression let createPerson name age email = result { let! validName = validateName name let! validAge = validateAge age let! validEmail = validateEmail email return { FirstName = validName LastName = "" Age = validAge } } // Usage match createPerson "Alice" 30 "alice@example.com" with | Ok person -> printfn $"Created: {person.FirstName}" | Error msg -> printfn $"Validation failed: {msg}"
Custom Error Types
// Domain-specific errors type ValidationError = | InvalidEmail of string | InvalidAge of int | InvalidName of string type PaymentError = | InsufficientFunds of decimal | CardExpired of System.DateTime | NetworkError of string // Using custom errors let validateEmail email : Result<string, ValidationError> = if email.Contains("@") then Ok email else Error (InvalidEmail email) // Combining different error types type AppError = | Validation of ValidationError | Payment of PaymentError let processPayment email amount = result { let! validEmail = validateEmail email |> Result.mapError Validation return "Payment successful" }
Applicative Validation
Collect all errors instead of stopping at first failure.
// Validation that accumulates errors type Validation<'T> = Result<'T, string list> let validatePersonApplicative name age email : Validation<Person> = let nameResult = if String.IsNullOrWhiteSpace(name) then Error ["Name cannot be empty"] else Ok name let ageResult = if age >= 0 && age <= 120 then Ok age else Error ["Age must be between 0 and 120"] let emailResult = if email.Contains("@") then Ok email else Error ["Invalid email format"] // Combine all results match nameResult, ageResult, emailResult with | Ok n, Ok a, Ok e -> Ok { FirstName = n; LastName = ""; Age = a } | _ -> // Collect all errors [nameResult; ageResult; emailResult] |> List.choose (function Error errs -> Some errs | Ok _ -> None) |> List.concat |> Error
Concurrency
F# provides first-class support for asynchronous and concurrent programming through async workflows, Task integration, and the MailboxProcessor (agent) model.
Async Workflows
// Basic async computation let fetchData url = async { printfn $"Fetching {url}..." do! Async.Sleep 1000 // Async sleep return $"Data from {url}" } // Run async computation let data = fetchData "https://api.example.com" |> Async.RunSynchronously // Parallel composition let processParallel urls = async { let! results = urls |> List.map fetchData |> Async.Parallel return results |> Array.toList }
Error Handling in Async
// Async with Result let safeFetchData url = async { try let! data = fetchData url return Ok data with | ex -> return Error ex.Message } // Catching specific exceptions let fetchWithRetry url maxRetries = async { let rec loop retriesLeft = async { try let! data = fetchData url return Ok data with | :? System.Net.WebException when retriesLeft > 0 -> do! Async.Sleep 1000 return! loop (retriesLeft - 1) | ex -> return Error $"Failed after {maxRetries} retries: {ex.Message}" } return! loop maxRetries }
Async.StartChild (Child Workflows)
// Start child async within parent workflow let parentWorkflow = async { // Start child but don't wait yet let! childComputation = Async.StartChild(async { do! Async.Sleep 1000 return 42 }) // Do other work while child runs printfn "Parent doing other work..." do! Async.Sleep 500 // Now wait for child result let! result = childComputation return result * 2 } // With timeout for child let parentWithTimeout = async { let! childComputation = Async.StartChild(longRunningAsync, millisecondsTimeout = 5000) try let! result = childComputation return Ok result with | :? System.TimeoutException -> return Error "Child computation timed out" }
Task Integration
// Interop with .NET Task open System.Threading.Tasks // Task computation expression (F# 6+) let fetchDataTask url = task { printfn $"Fetching {url}..." do! Task.Delay 1000 return $"Data from {url}" } // Convert between Async and Task let asyncToTask = fetchData "url" |> Async.StartAsTask let taskToAsync = fetchDataTask "url" |> Async.AwaitTask
Task vs Async Comparison
| Aspect | Async | Task |
|---|---|---|
| Execution | Cold (doesn't start until run) | Hot (starts immediately) |
| Composition | Easy with | F# 6+ builder |
| Cancellation | Built-in via CancellationToken | Via CancellationToken |
| Exception handling | Wrapped in Async | Direct exceptions |
| .NET interop | Convert with | Native .NET |
| Performance | Slight overhead | Better for .NET interop |
| Use when | F#-centric code, composition | C# interop, ASP.NET Core |
// Prefer Async for F# composition let processDataAsync inputs = async { let! results = inputs |> List.map processItemAsync |> Async.Parallel return Array.toList results } // Prefer Task for ASP.NET Core controllers let handleRequest (ctx: HttpContext) = task { let! data = ctx.Request.ReadFromJsonAsync<InputData>() let result = processData data return! ctx.Response.WriteAsJsonAsync(result) }
MailboxProcessor (Agents)
The MailboxProcessor provides a message-based concurrency model, similar to Erlang's actors.
// Simple counter agent type CounterMessage = | Increment | Decrement | Get of AsyncReplyChannel<int> let counterAgent = MailboxProcessor.Start(fun inbox -> let rec loop count = async { let! msg = inbox.Receive() match msg with | Increment -> return! loop (count + 1) | Decrement -> return! loop (count - 1) | Get replyChannel -> replyChannel.Reply(count) return! loop count } loop 0) // Using the agent counterAgent.Post Increment counterAgent.Post Increment let count = counterAgent.PostAndReply(fun reply -> Get reply) // 2
Agent Patterns
// Request-reply pattern type CacheMessage<'K, 'V> = | Get of key: 'K * AsyncReplyChannel<'V option> | Set of key: 'K * value: 'V | Clear let createCache<'K, 'V when 'K: comparison>() = MailboxProcessor.Start(fun inbox -> let rec loop (cache: Map<'K, 'V>) = async { let! msg = inbox.Receive() match msg with | Get (key, replyChannel) -> replyChannel.Reply(Map.tryFind key cache) return! loop cache | Set (key, value) -> return! loop (Map.add key value cache) | Clear -> return! loop Map.empty } loop Map.empty) // Usage let cache = createCache<string, int>() cache.Post (Set ("key1", 42)) let value = cache.PostAndReply(fun reply -> Get ("key1", reply)) // Some 42
CancellationToken Support
open System.Threading // Async with cancellation let longRunningTask = async { for i in 1..100 do printfn $"Step {i}" do! Async.Sleep 100 return "Completed" } // Create cancellation token let cts = new CancellationTokenSource() // Start with cancellation Async.Start(longRunningTask, cts.Token) // Cancel after 500ms Thread.Sleep 500 cts.Cancel()
Timeout Patterns
open System open System.Threading // Timeout with CancellationTokenSource let withTimeout milliseconds computation = async { use cts = new CancellationTokenSource(milliseconds) try let! result = Async.StartChild(computation, millisecondsTimeout = milliseconds) let! value = result return Ok value with | :? TimeoutException -> return Error "Operation timed out" | :? OperationCanceledException -> return Error "Operation cancelled" } // Using Async.RunSynchronously with timeout let resultWithTimeout = try fetchData "url" |> Async.RunSynchronously |> Some with | :? TimeoutException -> None // Task timeout with Task.WhenAny let withTaskTimeout (timeout: TimeSpan) (task: Task<'T>) = task { use cts = new CancellationTokenSource() let delayTask = Task.Delay(timeout, cts.Token) let! completedTask = Task.WhenAny(task, delayTask) if completedTask = (task :> Task) then cts.Cancel() return Ok (task.Result) else return Error "Task timed out" } // Combine cancellation and timeout let fetchWithCancellationAndTimeout url (parentToken: CancellationToken) = async { use cts = CancellationTokenSource.CreateLinkedTokenSource(parentToken) cts.CancelAfter(5000) // 5 second timeout try let! result = fetchData url return Ok result with | :? OperationCanceledException when parentToken.IsCancellationRequested -> return Error "Parent cancelled" | :? OperationCanceledException -> return Error "Timed out" }
Parallel Processing
// Parallel map (CPU-bound) let numbers = [1..1000000] let results = numbers |> Array.ofList |> Array.Parallel.map (fun x -> x * x) // Parallel for open System.Threading.Tasks Parallel.For(0, 100, fun i -> printfn $"Iteration {i}" ) |> ignore
Concurrency Model Comparison
| Model | Use Case | Characteristics |
|---|---|---|
| Async | I/O-bound operations | Cooperative, composable, cold start |
| Task | .NET interop, hot operations | Eager, integrates with C# |
| MailboxProcessor | Stateful agents, actors | Message-passing, thread-safe state |
| Array.Parallel | CPU-bound data parallelism | Fork-join, automatic partitioning |
| Parallel.For | Fine-grained CPU parallelism | Loop-level parallelism |
| Channels | Producer-consumer | Bounded/unbounded queues |
// Choosing the right model let ioWorkload urls = // Use Async for I/O-bound work urls |> List.map fetchAsync |> Async.Parallel |> Async.RunSynchronously let cpuWorkload data = // Use Array.Parallel for CPU-bound work data |> Array.Parallel.map expensiveComputation let statefulProcessor () = // Use MailboxProcessor for mutable state MailboxProcessor.Start(fun inbox -> let rec loop state = async { let! msg = inbox.Receive() let newState = processMessage state msg return! loop newState } loop initialState)
Metaprogramming
F# provides powerful metaprogramming capabilities through type providers, quotations, computation expressions, and active patterns.
Type Providers
Type providers generate types at compile-time based on external data sources, providing type-safe access to structured data.
// CSV Type Provider open FSharp.Data type Stocks = CsvProvider<"sample.csv"> let data = Stocks.Load("data.csv") for row in data.Rows do printfn $"{row.Date}: ${row.Close}" // JSON Type Provider type Weather = JsonProvider<""" { "city": "Seattle", "temperature": 72, "conditions": ["partly cloudy", "windy"] } """> let weather = Weather.Load("weather.json") printfn $"{weather.City}: {weather.Temperature}°F"
Quotations
Quotations represent F# code as abstract syntax trees (AST) for manipulation and analysis.
open Microsoft.FSharp.Quotations // Code quotations let expr = <@ 1 + 2 @> // Typed quotation let rawExpr = <@@ 1 + 2 @@> // Untyped quotation // Examining quotations let rec printExpr expr = match expr with | Patterns.Value(v, _) -> printfn $"Value: {v}" | Patterns.Call(_, mi, args) -> printfn $"Call: {mi.Name}" args |> List.iter printExpr | Patterns.Lambda(var, body) -> printfn $"Lambda: {var.Name}" printExpr body | _ -> printfn "Other pattern"
Quotation Patterns
open Microsoft.FSharp.Quotations.Patterns open Microsoft.FSharp.Quotations.DerivedPatterns // Pattern matching on quotations let rec evaluate expr = match expr with | Value(v, t) when t = typeof<int> -> v :?> int | Call(None, mi, [left; right]) when mi.Name = "op_Addition" -> evaluate left + evaluate right | Call(None, mi, [left; right]) when mi.Name = "op_Multiply" -> evaluate left * evaluate right | _ -> failwith "Unsupported expression" // Usage let result = evaluate <@ 2 + 3 * 4 @> // 14
Computation Expressions
Computation expressions provide syntactic sugar for monadic operations.
// Custom computation expression builder type MaybeBuilder() = member _.Bind(x, f) = Option.bind f x member _.Return(x) = Some x member _.ReturnFrom(x) = x member _.Zero() = None member _.Delay(f) = f member _.Run(f) = f() let maybe = MaybeBuilder() // Usage let result = maybe { let! x = Some 5 let! y = Some 10 return x + y } // Some 15
Advanced Active Patterns
// Parameterized active patterns let (|DivisibleBy|_|) divisor n = if n % divisor = 0 then Some () else None match 15 with | DivisibleBy 3 -> "Divisible by 3" | DivisibleBy 5 -> "Divisible by 5" | _ -> "Not divisible" // Multi-case with computation let (|Small|Medium|Large|Huge|) value = if value < 10 then Small elif value < 100 then Medium value elif value < 1000 then Large value else Huge value match 150 with | Small -> "small" | Medium x -> $"medium: {x}" | Large x -> $"large: {x}" | Huge -> "huge"
Reflection
open System.Reflection // Get type information let t = typeof<Person> printfn $"Type: {t.Name}" printfn $"Properties: {t.GetProperties().Length}" // Create instance dynamically let createInstance (t: System.Type) = System.Activator.CreateInstance(t) // Invoke method let invokeMethod obj methodName args = let t = obj.GetType() let mi = t.GetMethod(methodName) mi.Invoke(obj, args)
Zero/Default Values
F# handles default and zero values differently than C#, with a strong emphasis on explicit handling through Option types rather than null references.
Option.None vs Null
// Preferred: Use Option for optional values type Person = { Name: string Email: string option // Explicitly optional Age: int } let person = { Name = "Alice" Email = None // Explicit absence Age = 30 } // Pattern matching makes absence explicit match person.Email with | Some email -> printfn $"Email: {email}" | None -> printfn "No email provided"
Default Values in Records
// Records don't have default constructors type Config = { Host: string Port: int EnableLogging: bool } // Create default config explicitly let defaultConfig = { Host = "localhost" Port = 8080 EnableLogging = false } // Smart constructor pattern for defaults module Config = let create() = { Host = "localhost" Port = 8080 EnableLogging = false } let withHost host config = { config with Host = host } let withPort port config = { config with Port = port } // Usage with fluent API let config = Config.create() |> Config.withHost "api.example.com" |> Config.withPort 443
Unchecked.defaultof<'T>
// Get CLR default value for a type let defaultInt = Unchecked.defaultof<int> // 0 let defaultBool = Unchecked.defaultof<bool> // false let defaultString = Unchecked.defaultof<string> // null let defaultOption = Unchecked.defaultof<int option> // None // Useful for interop with .NET APIs type MyClass() = let mutable value: string = Unchecked.defaultof<string> member _.Value with get() = value and set(v) = value <- v
Null Handling for Interop
// Checking for null from .NET let safeToUpper (s: string) = if isNull s then None else Some (s.ToUpper()) // Option.ofObj: convert null to None let fromNullable (s: string) = Option.ofObj s let result = fromNullable null // None let result2 = fromNullable "test" // Some "test" // Option.toObj: convert None to null let toNullable opt = Option.toObj opt let nullable = toNullable None // null let nullable2 = toNullable (Some "test") // "test"
Array and Collection Defaults
// Empty collections let emptyList = [] let emptyArray = [||] let emptySeq = Seq.empty let emptyMap = Map.empty let emptySet = Set.empty // Arrays initialized with default values let zeros = Array.zeroCreate<int> 10 // [|0; 0; 0; ...|] let defaults = Array.create 5 "default" // [|"default"; "default"; ...|] // Using init for custom defaults let squares = Array.init 5 (fun i -> i * i) // [|0; 1; 4; 9; 16|]
Nullable<'T> for Value Types
open System // Interop with .NET Nullable let nullableInt: Nullable<int> = Nullable() // No value let nullableInt2: Nullable<int> = Nullable(42) // Has value // Check for value if nullableInt.HasValue then printfn $"Value: {nullableInt.Value}" else printfn "No value" // Convert to/from Option let fromNullable (n: Nullable<'T>) : 'T option = if n.HasValue then Some n.Value else None let toNullable (opt: 'T option) : Nullable<'T> = match opt with | Some v -> Nullable(v) | None -> Nullable()
Best Practices
// Do: Use Option for optional values type User = { Name: string Email: string option // Explicit Age: int } // Don't: Use null for optional values type BadUser = { Name: string Email: string // Could be null - unclear Age: int } // Do: Provide explicit default functions module User = let create name = { Name = name Email = None Age = 0 } // Don't: Rely on Unchecked.defaultof unless necessary let badDefault = Unchecked.defaultof<User> // Null, not a valid User
Common Idioms
Dependency Injection (Reader Pattern)
// Pass dependencies explicitly type Dependencies = { GetTime: unit -> System.DateTime Logger: string -> unit } let processOrder deps orderId = deps.Logger $"Processing order {orderId}" let timestamp = deps.GetTime() // ... rest of logic // Usage let deps = { GetTime = fun () -> System.DateTime.Now Logger = printfn "%s" } processOrder deps 123
Event Sourcing Pattern
// Define events type AccountEvent = | AccountOpened of customerId: string * initialBalance: decimal | MoneyDeposited of amount: decimal | MoneyWithdrawn of amount: decimal // Apply events to state type Account = { Balance: decimal } let apply state event = match event with | AccountOpened (_, initialBalance) -> { Balance = initialBalance } | MoneyDeposited amount -> { state with Balance = state.Balance + amount } | MoneyWithdrawn amount -> { state with Balance = state.Balance - amount } // Rebuild state from events let buildState events = events |> List.fold apply { Balance = 0m }
Troubleshooting
Type Inference Issues
Problem: Compiler can't infer types
// Error: type inference failure let processItems items = items |> List.map (fun x -> x.ToString())
Fix: Add type annotations
let processItems (items: int list) = items |> List.map (fun x -> x.ToString())
Option/Result Unwrapping
Problem: Nested Option/Result types
// Bad: nested Some let findAndProcess id = match find id with | Some person -> match process person with | Some result -> Some result | None -> None | None -> None
Fix: Use computation expressions or bind
let findAndProcess id = find id |> Option.bind process
Mutable vs Immutable
Problem: Need to update values
// Doesn't work - records are immutable let person = { FirstName = "John"; LastName = "Doe"; Age = 30 } person.Age <- 31 // Error!
Fix: Use copy-and-update or mutable
// Functional: create new record let olderPerson = { person with Age = 31 } // Or use mutable if really needed type MutablePerson = { FirstName: string LastName: string mutable Age: int }
Recursive Type Definitions
Problem: Types reference each other
// Error: types not defined yet type Folder = { Name: string; Items: Item list } type Item = File of string | Folder of Folder
Fix: Use
and keyword
type Folder = { Name: string; Items: Item list } and Item = File of string | Folder of Folder
Serialization
F# works seamlessly with .NET serialization libraries while maintaining functional idioms. For cross-language serialization patterns, see
patterns-serialization-dev.
System.Text.Json
open System.Text.Json open System.Text.Json.Serialization // Simple record serialization type Person = { FirstName: string LastName: string Age: int } let person = { FirstName = "Alice"; LastName = "Smith"; Age = 30 } let json = JsonSerializer.Serialize(person) // {"FirstName":"Alice","LastName":"Smith","Age":30} let parsed = JsonSerializer.Deserialize<Person>(json) // Custom options let options = JsonSerializerOptions() options.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase options.WriteIndented <- true let camelCaseJson = JsonSerializer.Serialize(person, options) // { // "firstName": "alice", // "lastName": "smith", // "age": 30 // }
JsonFSharpConverter for F# Types
open System.Text.Json open System.Text.Json.Serialization // Configure for F# discriminated unions and options let options = JsonSerializerOptions() options.Converters.Add(JsonFSharpConverter()) // Discriminated union serialization type PaymentMethod = | Cash | CreditCard of cardNumber: string | DebitCard of cardNumber: string * pin: int let payment = CreditCard "1234-5678-9012-3456" let json = JsonSerializer.Serialize(payment, options) // {"Case":"CreditCard","Fields":["1234-5678-9012-3456"]} // Option type handling type User = { Name: string Email: string option } let user = { Name = "Bob"; Email = Some "bob@example.com" } let userJson = JsonSerializer.Serialize(user, options)
Custom Converters
open System open System.Text.Json open System.Text.Json.Serialization // Custom converter for EmailAddress type EmailAddress = EmailAddress of string type EmailAddressConverter() = inherit JsonConverter<EmailAddress>() override _.Read(reader, typeToConvert, options) = EmailAddress (reader.GetString()) override _.Write(writer, value, options) = let (EmailAddress email) = value writer.WriteStringValue(email) // Register converter let options = JsonSerializerOptions() options.Converters.Add(EmailAddressConverter()) let email = EmailAddress "test@example.com" let json = JsonSerializer.Serialize(email, options) // "test@example.com"
FSharp.Json
open FSharp.Json // Simple serialization type Config = { Port: int Host: string Debug: bool } let config = { Port = 8080; Host = "localhost"; Debug = true } let json = Json.serialize config let parsed = Json.deserialize<Config> json // Custom field names type ApiResponse = { [<JsonField("response_code")>] ResponseCode: int [<JsonField("response_data")>] Data: string } // Transform during serialization type Settings = { [<JsonField(Transform=typeof<Transforms.CamelCase>)>] DatabaseUrl: string [<JsonField(Transform=typeof<Transforms.SnakeCase>)>] MaxConnections: int }
Type Providers for JSON
open FSharp.Data // Infer schema from sample JSON type Weather = JsonProvider<""" { "temperature": 72.5, "condition": "sunny", "humidity": 65, "forecast": [ {"day": "Monday", "high": 75, "low": 60}, {"day": "Tuesday", "high": 78, "low": 62} ] } """> // Use with type safety let weather = Weather.Load("weather.json") printfn $"Temperature: {weather.Temperature}°F" printfn $"Condition: {weather.Condition}" weather.Forecast |> Array.iter (fun day -> printfn $"{day.Day}: {day.High}°F / {day.Low}°F") // From URL let liveWeather = Weather.Load("https://api.weather.com/current") // Parse from string let jsonString = """{"temperature":68,"condition":"cloudy","humidity":70}""" let parsed = Weather.Parse(jsonString)
Validation Patterns
// Validation with Result type ValidationError = string let validateEmail (email: string) : Result<string, ValidationError> = if email.Contains("@") then Ok email else Error "Invalid email format" let validateAge (age: int) : Result<int, ValidationError> = if age >= 0 && age <= 120 then Ok age else Error "Age must be between 0 and 120" // Combine validations type PersonData = { Name: string Email: string Age: int } let validatePerson data = result { let! validEmail = validateEmail data.Email let! validAge = validateAge data.Age return { data with Email = validEmail; Age = validAge } } // Applicative validation (collect all errors) type Validation<'T> = Result<'T, ValidationError list> let validatePersonApplicative data : Validation<PersonData> = let validateName name = if String.IsNullOrWhiteSpace(name) then Error ["Name cannot be empty"] else Ok name match (validateName data.Name, validateEmail data.Email, validateAge data.Age) with | Ok n, Ok e, Ok a -> Ok { Name = n; Email = e; Age = a } | errors -> errors |> fun (n, e, a) -> [n; e; a] |> List.collect (function Error errs -> errs | Ok _ -> []) |> Error
Build and Dependencies
F# uses the standard .NET build ecosystem with project files, NuGet packages, and the dotnet CLI.
Project File (.fsproj)
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <OutputType>Exe</OutputType> <RootNamespace>MyApp</RootNamespace> </PropertyGroup> <ItemGroup> <!-- Order matters in F#! Files are compiled top-to-bottom --> <Compile Include="Types.fs" /> <Compile Include="Helpers.fs" /> <Compile Include="Domain.fs" /> <Compile Include="Program.fs" /> </ItemGroup> <ItemGroup> <PackageReference Include="FSharp.Data" Version="6.3.0" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> </ItemGroup> </Project>
dotnet CLI Commands
# Create new project dotnet new console -lang F# -o MyApp dotnet new classlib -lang F# -o MyLib dotnet new webapi -lang F# -o MyApi # Build and run dotnet build dotnet run dotnet run -- arg1 arg2 # Pass arguments # Watch mode (rebuild on file changes) dotnet watch run # Clean build artifacts dotnet clean # Restore packages dotnet restore # Create solution dotnet new sln -n MySolution dotnet sln add MyApp/MyApp.fsproj dotnet sln add MyLib/MyLib.fsproj
NuGet Package Management
# Add package dotnet add package FSharp.Data dotnet add package Newtonsoft.Json --version 13.0.3 # Remove package dotnet remove package FSharp.Data # Update package dotnet add package FSharp.Data # Gets latest # List packages dotnet list package # List outdated packages dotnet list package --outdated
Package References in .fsproj
<ItemGroup> <!-- Exact version --> <PackageReference Include="FSharp.Core" Version="8.0.0" /> <!-- Version range --> <PackageReference Include="FSharp.Data" Version="[6.0,7.0)" /> <!-- Latest stable --> <PackageReference Include="Newtonsoft.Json" Version="*" /> <!-- Development only --> <PackageReference Include="FsCheck" Version="2.16.5"> <PrivateAssets>all</PrivateAssets> </PackageReference> </ItemGroup>
Project References
<!-- Reference another project --> <ItemGroup> <ProjectReference Include="..\MyLib\MyLib.fsproj" /> </ItemGroup>
# Add project reference via CLI dotnet add reference ../MyLib/MyLib.fsproj
Paket (Alternative Package Manager)
# Install Paket dotnet tool install paket # Initialize Paket dotnet paket init # Add package dotnet paket add FSharp.Data # Install dependencies dotnet paket install # Update all packages dotnet paket update
paket.dependencies:
source https://api.nuget.org/v3/index.json nuget FSharp.Core >= 8.0 nuget FSharp.Data ~> 6.3 nuget Newtonsoft.Json group Test nuget Expecto nuget FsCheck
paket.references:
FSharp.Data Newtonsoft.Json group Test Expecto FsCheck
FAKE Build Script
// build.fsx #r "paket: nuget Fake.Core.Target nuget Fake.DotNet.Cli //" #load ".fake/build.fsx/intellisense.fsx" open Fake.Core open Fake.DotNet let clean _ = !! "**/bin" ++ "**/obj" |> Shell.cleanDirs let build _ = DotNet.build id "" let test _ = DotNet.test id "" let publish _ = DotNet.publish (fun opts -> { opts with Configuration = DotNet.BuildConfiguration.Release OutputPath = Some "./publish" }) "" // Define targets Target.create "Clean" clean Target.create "Build" build Target.create "Test" test Target.create "Publish" publish // Dependencies open Fake.Core.TargetOperators "Clean" ==> "Build" ==> "Test" ==> "Publish" Target.runOrDefault "Build"
Run with:
dotnet fake build dotnet fake build -t Publish
Multi-Project Structure
MySolution/ ├── MySolution.sln ├── src/ │ ├── MyApp/ │ │ ├── MyApp.fsproj │ │ ├── Program.fs │ │ └── Domain.fs │ └── MyLib/ │ ├── MyLib.fsproj │ ├── Types.fs │ └── Utils.fs ├── tests/ │ └── MyApp.Tests/ │ ├── MyApp.Tests.fsproj │ └── Tests.fs ├── paket.dependencies └── build.fsx
Publishing
# Publish for specific runtime dotnet publish -r win-x64 -c Release dotnet publish -r linux-x64 -c Release dotnet publish -r osx-arm64 -c Release # Self-contained (includes runtime) dotnet publish -r linux-x64 -c Release --self-contained # Framework-dependent (requires .NET runtime installed) dotnet publish -c Release --no-self-contained # Single file dotnet publish -r linux-x64 -c Release /p:PublishSingleFile=true
NuGet Package Creation
<!-- Add to .fsproj --> <PropertyGroup> <PackageId>MyAwesomeLibrary</PackageId> <Version>1.0.0</Version> <Authors>Your Name</Authors> <Description>An awesome F# library</Description> <PackageLicenseExpression>MIT</PackageLicenseExpression> <RepositoryUrl>https://github.com/username/repo</RepositoryUrl> </PropertyGroup>
# Create package dotnet pack -c Release # Publish to NuGet dotnet nuget push bin/Release/MyAwesomeLibrary.1.0.0.nupkg --api-key YOUR_KEY --source https://api.nuget.org/v3/index.json
Testing
F# has excellent testing support with Expecto, FsUnit, and FsCheck for property-based testing.
Expecto
// Tests.fs module Tests open Expecto // Simple test let simpleTest = testCase "addition works" <| fun () -> let result = 1 + 1 Expect.equal result 2 "1 + 1 should equal 2" // Test list let mathTests = testList "Math operations" [ testCase "addition" <| fun () -> Expect.equal (2 + 2) 4 "2 + 2 = 4" testCase "subtraction" <| fun () -> Expect.equal (5 - 3) 2 "5 - 3 = 2" testCase "multiplication" <| fun () -> Expect.equal (3 * 4) 12 "3 * 4 = 12" ] // Run all tests [<EntryPoint>] let main args = runTestsWithCLIArgs [] args mathTests
Expecto Matchers
open Expecto let expectTests = testList "Expecto expectations" [ testCase "equal" <| fun () -> Expect.equal (1 + 1) 2 "should be equal" testCase "not equal" <| fun () -> Expect.notEqual 1 2 "should not be equal" testCase "is true" <| fun () -> Expect.isTrue (5 > 3) "5 should be greater than 3" testCase "is false" <| fun () -> Expect.isFalse (3 > 5) "3 should not be greater than 5" testCase "contains" <| fun () -> Expect.contains [1; 2; 3] 2 "list should contain 2" testCase "sequence equal" <| fun () -> Expect.sequenceEqual [1; 2; 3] [1; 2; 3] "sequences should match" testCase "throws" <| fun () -> Expect.throws (fun () -> failwith "boom") "should throw" testCase "is some" <| fun () -> Expect.isSome (Some 5) "should be Some" testCase "is none" <| fun () -> Expect.isNone None "should be None" ]
Async and Task Testing
open Expecto let asyncTests = testList "Async tests" [ testCaseAsync "async computation" <| async { let! result = async { return 42 } Expect.equal result 42 "async result" } testTask "task computation" { let! result = task { return 42 } Expect.equal result 42 "task result" } ]
Test Organization
// Nested test groups let allTests = testList "All tests" [ testList "Domain" [ testList "User" [ testCase "create user" <| fun () -> let user = createUser "Alice" "alice@example.com" Expect.equal user.Name "Alice" "name matches" ] testList "Order" [ testCase "calculate total" <| fun () -> let total = calculateTotal [10m; 20m; 30m] Expect.equal total 60m "total is sum" ] ] testList "API" [ // API tests ] ] // Run with filters [<EntryPoint>] let main args = runTestsWithCLIArgs [] args allTests // Run specific tests: // dotnet run -- --filter "User"
FsUnit with xUnit
module Tests open Xunit open FsUnit.Xunit [<Fact>] let ``2 + 2 should equal 4`` () = 2 + 2 |> should equal 4 [<Fact>] let ``list should contain element`` () = [1; 2; 3] |> should contain 2 [<Fact>] let ``string should start with`` () = "hello world" |> should startWith "hello" [<Fact>] let ``option should be Some`` () = Some 5 |> should be (ofCase <@ Some @>) [<Theory>] [<InlineData(1, 2, 3)>] [<InlineData(5, 5, 10)>] [<InlineData(-1, 1, 0)>] let ``addition works for multiple inputs`` a b expected = a + b |> should equal expected
FsCheck Property-Based Testing
open Expecto open FsCheck // Property test with Expecto let propertyTests = testList "Property tests" [ testProperty "reverse twice equals original" <| fun (xs: int list) -> List.rev (List.rev xs) = xs testProperty "length of reverse equals length" <| fun (xs: int list) -> List.length (List.rev xs) = List.length xs testProperty "addition is commutative" <| fun (a: int) (b: int) -> a + b = b + a testProperty "list append length" <| fun (xs: int list) (ys: int list) -> List.length (xs @ ys) = List.length xs + List.length ys ] // Custom generator let positiveInt = Arb.generate<int> |> Gen.map abs let customGeneratorTest = testProperty "square of positive is positive" <| fun () -> Prop.forAll (Arb.fromGen positiveInt) (fun n -> n * n >= 0) // Conditional properties let conditionalTest = testProperty "division by non-zero" <| fun (a: float) (b: float) -> b <> 0.0 ==> lazy (a / b * b = a)
FsCheck with xUnit
open Xunit open FsCheck open FsCheck.Xunit [<Property>] let ``reverse twice gives original`` (xs: int list) = List.rev (List.rev xs) = xs [<Property>] let ``sort is idempotent`` (xs: int list) = List.sort (List.sort xs) = List.sort xs [<Property(Arbitrary = [| typeof<CustomGenerators> |])>] let ``custom generator property`` (email: string) = email.Contains("@") // Custom generators type CustomGenerators = static member Email() = let genEmail = gen { let! user = Gen.elements ["alice"; "bob"; "charlie"] let! domain = Gen.elements ["example.com"; "test.com"] return $"{user}@{domain}" } Arb.fromGen genEmail
Test Setup and Teardown
open Expecto // Setup/teardown pattern let withDatabase test = let db = setupDatabase() // Setup try test db finally cleanupDatabase db // Teardown let databaseTests = testList "Database tests" [ testCase "insert user" <| fun () -> withDatabase (fun db -> insertUser db "Alice" let users = getUsers db Expect.contains users "Alice" "user should exist" ) ] // Shared fixture type DatabaseFixture() = let db = setupDatabase() member _.Database = db interface System.IDisposable with member _.Dispose() = cleanupDatabase db let fixtureTests = testSequenced <| testList "Sequenced tests" [ let fixture = new DatabaseFixture() yield testCase "test 1" <| fun () -> // use fixture.Database () yield testCase "test 2" <| fun () -> // use fixture.Database () ]
Mocking and Stubs
// Interface-based mocking type IUserRepository = abstract member GetUser: int -> User option abstract member SaveUser: User -> unit // Create stub for testing let createStubRepository users = { new IUserRepository with member _.GetUser(id) = users |> List.tryFind (fun u -> u.Id = id) member _.SaveUser(user) = () // No-op for testing } let testWithStub = testCase "get user from stub" <| fun () -> let users = [ { Id = 1; Name = "Alice"; Email = "alice@example.com"; Age = 30 } { Id = 2; Name = "Bob"; Email = "bob@example.com"; Age = 25 } ] let repo = createStubRepository users let user = repo.GetUser(1) Expect.isSome user "should find user" Expect.equal user.Value.Name "Alice" "name should match"
Running Tests
# Run with dotnet dotnet run # If entry point is defined dotnet test # If using xUnit/NUnit # Expecto options dotnet run -- --help dotnet run -- --filter "User" dotnet run -- --sequenced dotnet run -- --debug dotnet run -- --fail-on-focused-tests # Watch mode dotnet watch run
Cross-Cutting Patterns
For cross-language comparison and translation patterns, see:
- JSON/YAML handling, validation patternspatterns-serialization-dev
- Async workflows, parallel processing, Mailbox processorspatterns-concurrency-dev
- Type providers, computation expressions, quotationspatterns-metaprogramming-dev