Advanced F# Techniques: Patterns, Pipelines, and Performance
F# is a concise, expressive functional-first language on .NET that excels at modeling complex domains, composing transformations, and delivering high-performance code. This article covers advanced techniques—pattern matching, pipeline-centric design, and performance tuning—so you can write clearer, faster F# for production systems.
1. Pattern Matching: Expressive, Safe Branching
- Use discriminated unions (DUs) to model domain states and make illegal states unrepresentable.
fsharp
type Order = | New of id:intitems:string list | Processing of id:int * progress:int | Completed of id:int * timestamp:System.DateTime | Cancelled of id:int * reason:string - Exhaustive matching enforces handling all cases:
fsharp
- Use active patterns for custom deconstruction and readable matches:
fsharp
let (|Even|Odd|) x = if x % 2 = 0 then Even else Oddmatch n with| Even -> …| Odd -> … - Partial active patterns expose selective cases:
fsharp
let (|Int|_|) (s:string) = match System.Int32.TryParse(s) with true,i -> Some i | _ -> Nonematch “123” with| Int i -> printfn “%d” i| _ -> () - Combine pattern matching with when-guards and nested patterns for concise conditionals.
2. Pipelines and Point-Free Composition
- Favor pipelines (|>) and function composition (>>) to express data transformations as linear flows.
fsharp
let process = parse >> validate >> transform >> persist let result = input |> process - Use partial application and curried functions to build reusable steps:
fsharp
let multiplyBy x y = x * ylet double = multiplyBy 2[1;2;3] |> List.map double - Prefer Seq/Array/List module functions that return sequences to work with lazy evaluation where appropriate (Seq) to improve memory behavior.
- Leverage pipelines with tasks/async:
fsharp
let fetchAndProcess url = async { let! text = httpGetAsync url return text |> parse |> analyze } - Use pipeline-friendly error handling with Result and computation expressions:
fsharp
let bind f = function Ok v -> f v | Error e -> Error elet (>=>) a b = fun x -> a x |> Result.bind b let workflow = parseResult >=> validateResult >=> computeResult
3. Performance: Writing Fast F#
- Prefer immutable data but avoid excessive allocations in hot paths—use arrays or Span where needed.
- Choose the right collection: List for recursive functional patterns, Array for tight loops, ResizeArray/Collections.Generic.List for mutable builders.
- Use Seq only when laziness is required; otherwise use Array/List to avoid iterator overhead.
- Inline small functions to eliminate delegate overhead:
fsharp
[]let inline add a b = a + b - Use value tuples and structs to reduce heap allocations for short-lived data:
fsharp
let inline swap (x: ‘a, y: ‘b) = (y, x) - Prefer structs for small records used in tight loops:
fsharp
[]type Point = { X: float; Y: float } - Avoid boxing: keep types generic and constrained, or use interfaces sparingly in hot paths.
- Minimize closure allocations by avoiding capturing variables in inner lambdas in performance-critical loops.
- Use Span and Memory with System.Memory-friendly APIs and System.Buffers for pooling to reduce GC pressure.
- Benchmark with BenchmarkDotNet and profile with Perf
Leave a Reply