F# — Reference

Source: https://learn.microsoft.com/en-us/dotnet/fsharp/

F

  • Created: 2005 by Don Syme at Microsoft Research
  • Latest stable: F# 10 (ships with .NET 10 / Visual Studio 2026, November 2025)
  • Paradigms: Functional-first, multi-paradigm (functional, OO, imperative); strict by default
  • Typing: Strong static, Hindley-Milner with .NET object type integration; sound and inferred
  • Memory: .NET CLR garbage collector (generational, concurrent); immutable by default, opt-in mutability
  • Compilation: Compiled to .NET IL; runs on .NET (cross-platform), Mono historically; Fable transpiles to JS/TS/Python/Rust/Dart
  • Primary domains: Finance/quant, data science, web (Giraffe/Saturn), domain modeling, scripts/automation, Azure functions, machine learning
  • Official docs: https://learn.microsoft.com/en-us/dotnet/fsharp/

At a glance

F# is a functional-first member of the .NET family, designed for correctness via strong typing, immutability, and pattern matching while retaining full interop with the C# / .NET ecosystem. The language is governed jointly by Microsoft (compiler in dotnet/fsharp) and the F# Software Foundation (FSSF). Notable for type providers, computation expressions, units of measure, and a concise syntax that scales from 5-line scripts (.fsx) to large enterprise systems.

Getting started

Install — Install the .NET SDK from https://dotnet.microsoft.com/download. F# ships in every .NET SDK. Verify: dotnet fsi --version for F# Interactive.

Hello world (Program.fs):

printfn "Hello, world!"

Or as a script (hello.fsx): dotnet fsi hello.fsx.

Projectdotnet new console -lang F# -n MyApp scaffolds:

MyApp/
  MyApp.fsproj    # MSBuild XML
  Program.fs

File order in .fsproj matters: F# enforces top-down dependency, no forward references (a feature, not a bug). dotnet build, dotnet run, dotnet test. Use paket (community) or NuGet for deps; in scripts, #r "nuget: Newtonsoft.Json" works directly.

REPLdotnet fsi is F# Interactive. Statements end with ;;. #load "file.fsx", #r "nuget: pkg", #help. Excellent in editor (VS Code with Ionide, Visual Studio, Rider).

Basics

Types/literalsint (32-bit), int64, bigint (123I), float (= double, 64-bit), decimal (1.5m), bool, char, string, byte, unit (()), tuples ((1, "a")), lists ([1; 2; 3]), arrays ([|1; 2; 3|]), seq (seq { 1..10 }).

Variables/scopinglet x = 1 (immutable by default). let mutable x = 1 then x <- 2 for mutation. let inline for inlinable generic functions. Scope follows indentation (whitespace significant — like Python). Shadowing allowed: let x = 1; let x = x + 1.

Control flowif cond then a else b (an expression — both branches must have the same type), match x with | pat -> expr | _ -> default, while/do, for i in 1..10 do, try/with, try/finally. No statements — almost everything is an expression yielding unit if no value.

Functions — Curried by default: let add x y = x + y has type int -> int -> int. Lambda: fun x -> x * 2. Composition: f >> g (left-to-right, common) or g << f. Pipe: xs |> List.map f |> List.sum. Partial application is automatic.

Stringsstring is .NET System.String. sprintf "%d %s" n s (typed printf), interpolated $"hello {name}", triple-quoted """...""", verbatim @"C:\path". F# 5+ added $"..." interpolation; F# 8 added typed interpolation $"%d{n}".

Collectionslist (linked list, immutable, [1;2;3]), array ([|1;2;3|], mutable contents but fixed size), seq (lazy IEnumerable), Map<'K,'V> (immutable balanced tree), Set<'T>. Modules List, Array, Seq, Map, Set provide uniform map/filter/fold/choose. Records: type Point = { X: int; Y: int }. Discriminated unions (DUs): type Shape = Circle of float | Rect of float * float.

Intermediate

Type system depth — Algebraic types (records, DUs), generics, structural-ish equality on records and DUs auto-derived. Active patterns are user-defined match cases. SRTP (Statically Resolved Type Parameters, ^T) for compile-time generic dispatch. Units of measure (<m/s>) tag numeric types at compile time with no runtime cost. F# 10 added ValueOption struct optional parameters.

Modulesmodule MyMod = ... or namespace My.Ns + module Inner = .... Signature files .fsi declare a module’s public surface (analogous to .mli in OCaml).

Error handling — Three options: Result<'T, 'TError> for typed errors (idiomatic), Option<'T> for absence, .NET exceptions (raise, try ... with | :? IOException as e -> ...). Result.bind or computation expressions (result { ... } from FsToolkit.ErrorHandling) for chaining.

Concurrency primitives — Two systems coexist: async { ... } (F# native, cold/composable, exception-safe cancellation) and task { ... } (F# 6+, wraps .NET Task<'T> for C# interop, hot, what most modern code uses). Async.Parallel, Async.RunSynchronously, Async.StartChild. F# 10 adds and! to task expressions for concurrent await. MailboxProcessor<'T> is a built-in actor. System.Threading.Channels, System.Threading.Tasks.Dataflow from .NET.

I/O — Full .NET BCL: System.IO.File, Stream, StreamReader, HttpClient, System.Net.Sockets. F#-friendly wrappers in FSharp.Control.AsyncSeq. printfn/eprintfn for stdout/stderr.

Stdlib highlightsFSharp.Core provides List, Array, Seq, Map, Set, Option, Result, Async, Event, Observable, Choice, Lazy, computation expression builders. Plus everything in .NET BCL: LINQ, Span, Memory, Channels, etc.

Advanced

Memory/GC — .NET runtime: generational (gen0/1/2 + LOH/POH), concurrent and server modes. F# encourages immutable data; pooled mutable buffers (ArrayPool<T>) and Span<T>/Memory<T> available for hot paths. [<Struct>] attribute on records/DUs makes them value types (stack-allocated when possible). F# 10 adds [<Struct>] ValueOption for optional parameters to avoid allocation.

Concurrency deep diveasync is a continuation-passing structure: Async.RunSynchronously drives it. Cancellation tokens propagate through async blocks automatically; try/finally runs on cancellation. task is a thin wrapper over Task<'T> using state machines (since F# 6) — comparable perf to C# async/await. MailboxProcessor is a single-threaded async actor with PostAndAsyncReply for ask-pattern. Hopac is an alternative concurrency library based on Concurrent ML / John Reppy’s Hopac, faster than async for fine-grained concurrency.

FFI — Direct .NET interop covers C/C++/Rust via [<DllImport>] (P/Invoke). COM via System.Runtime.InteropServices. WASM via .NET 9+. JNI via IKVM (limited). Native AOT supported.

Reflection — Full .NET reflection (System.Reflection) plus F#-aware FSharp.Reflection for inspecting records/DUs/tuples/functions at runtime. FSharpType.GetUnionCases, FSharpValue.MakeRecord.

Performance toolsBenchmarkDotNet (industry standard for .NET microbench), dotnet-trace, dotnet-counters, dotnet-dump, PerfView, JetBrains dotTrace/dotMemory, EventPipe. F# 10 added parallel compilation (preview, opt-in via <ParallelCompilation>true</ParallelCompilation>).

God mode

Type providers — Compile-time code generators that materialize types from external schemas (SQL, JSON, XML, OData, CSV, R, GraphQL). Example: type Db = SqlDataProvider<...> makes every table a strongly typed property. Authored by implementing ITypeProvider. Killer feature for data work — eliminates ORM boilerplate. See https://learn.microsoft.com/en-us/dotnet/fsharp/tutorials/type-providers/.

Computation expressions — Custom workflow builders. The compiler desugars builder { let! x = e1; return x + 1 } into builder.Bind(e1, fun x -> builder.Return(x + 1)). Implement Bind, Return, Zero, Combine, Delay, Run, For, While, TryWith, TryFinally, Using, Yield, YieldFrom for full features. async, task, seq, query, option, result are all CEs. F# 10 added *Final methods for tail-call optimization in CEs (https://learn.microsoft.com/en-us/dotnet/fsharp/whats-new/fsharp-10).

Active patterns — Treat data via custom decompositions in match:

let (|Even|Odd|) n = if n % 2 = 0 then Even else Odd
match 7 with | Even -> "even" | Odd -> "odd"

Single-case ((|Hex|)), multi-case (above), partial ((|Foo|_|)), parameterized ((|Regex|_|) pattern).

Units of measure[<Measure>] type m, [<Measure>] type s, then let v: float<m/s> = 5.0<m/s>. Erased at compile time — zero runtime cost — but adding m/s to kg is a compile error. Famous Mars Climate Orbiter pitch.

SRTP (Statically Resolved Type Parameters)let inline add (x: ^T) (y: ^T) = x + y resolves operator overloads at compile time per call site. ^T : (static member Foo: int -> int) constraints. Powers seq { ... } and the Numerics abstractions.

F# quotations<@ x + 1 @> reifies an expression as Expr<int>. Expr.GetType(), pattern match against Patterns.*. Used by Fable (transpilation), database query DSLs, code analysis. <@@ ... @@> is the untyped variant.

Fable — F# to JavaScript (and TS, Python, Rust, Dart) compiler. Powers SAFE Stack (Saturn + Azure + Fable + Elmish) and pure F# frontends. https://fable.io.

F# Compiler Services API (FCS)FSharp.Compiler.Service exposes parser, type-checker, and project services; foundation of Ionide, Fantomas, Rider’s F# support, F# Analyzers SDK.

MSIL inspectionildasm (Windows SDK) or dotnet-ildasm, plus ILSpy for decompilation. [<MethodImpl(MethodImplOptions.AggressiveInlining)>] and inline keyword for control.

Idioms & style

  • Naming: camelCase for values/functions/modules, PascalCase for types/cases/properties, _ prefix for unused. Active patterns (|Foo|Bar|) PascalCase.
  • Formatter: Fantomas (https://fsprojects.github.io/fantomas/) — official-ish, integrates with all editors; dotnet tool install -g fantomas.
  • Linter: FSharpLint (community), F# Analyzers (custom rules via FCS).
  • Idiomatic: domain-model with records and DUs first; make illegal states unrepresentable (Scott Wlaschin’s mantra); Result over exceptions for expected failures; pipe (|>) for left-to-right reading; small modules; inline only when SRTP or perf demand it; prefer task over async for new code unless you need cancellation correctness or composition.
  • Expert review focus: file order in .fsproj (top-down dependencies); accidental .NET nullability leaks (null for ref types — F# 9 added nullable reference types); over-abstraction with CEs; mutable state hidden behind innocuous-looking F# code; FCS perf in tooling-heavy projects; type recursion via and.

Ecosystem

  • Web: Giraffe (functional middleware on ASP.NET Core), Saturn (Rails-like over Giraffe), Falco, Bolero (Blazor + Elmish for .NET WebAssembly), SAFE Stack (Saturn/Fable/Azure/Elmish).
  • Data/DB: SqlProvider (type provider), Dapper.FSharp, EF Core (works), Donald, FSharp.Data (CSV/JSON/HTML providers).
  • Testing: Expecto (idiomatic F#, the popular choice), xUnit/NUnit/MSTest (works fine), FsCheck (property-based), Unquote (assertion via quotations), FsUnit.
  • Async/Concurrency: built-in async/task, Hopac, AsyncSeq, FSharp.Control.TaskSeq.
  • Frontend: Fable + Elmish (React-like MVU pattern), Feliz (typed React DSL), Bolero.
  • Notebooks: .NET Interactive in Jupyter / Polyglot Notebooks; F# Notebook in VS Code.
  • Build: dotnet CLI, MSBuild, FAKE (F# Make — DSL build pipelines), Paket (alt to NuGet, lockfile-based).
  • Docs: FSharp.Formatting / fsdocs.
  • Notable users: Jet.com (Walmart), Credit Suisse, Bank of America (modeling), Linn Records, GameSys, Pluralsight, NRK, Tachyus, Ariba, .NET team itself (the F# compiler is written in F#).

Gotchas

  • File order in .fsproj — F# enforces top-down dependency; reorder files in the project file (right-click in IDE) to expose definitions to later files. No forward refs except via and in same file.
  • null and reference types — Pre-F# 9, .NET ref types could secretly be null even though F# code never produces it. F# 9+ has nullable reference types (string | null); enable with <Nullable>enable</Nullable>.
  • type aliases not opaquetype Email = string is just an alias; use single-case DU type Email = Email of string for nominal typing.
  • Mutually recursive types/funcs require and: type A = ... and B = ..., let rec f ... = ... and g ... = ....
  • Tuple syntax is (a, b) — comma is the constructor. f(a, b) is calling f with a single tuple, not two args; F# functions are curried by default unless declared with tupled parameters (common in .NET interop).
  • printfn is typedprintfn "%d" "hello" is a compile error; use %O for any object.
  • async vs taskasync is cold (won’t start until run); task is hot. Mixing them inside a CE is awkward.
  • do! inside seq { ... } is not allowed; sequences are pull-based.
  • Discriminated union casing collisions with module/type names; qualify with Cases.X if needed.
  • Reflection over inlined functions doesn’t work (they have no IL).
  • F# scripts vs projects#r "nuget: ..." works in .fsx; project deps go in .fsproj. Different package resolution.
  • Computation expression performance — older async had per-step allocation overhead; task (state-machine-based) is much faster.

Citations