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.
Project — dotnet 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.
REPL — dotnet 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/literals — int (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/scoping — let 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 flow — if 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.
Strings — string 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}".
Collections — list (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.
Modules — module 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 highlights — FSharp.Core provides List, Array, Seq, Map, Set, Option, Result, Async, Event, Observable, Choice, Lazy, computation expression builders. Plus everything in .NET BCL: LINQ, Span
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 dive — async 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 tools — BenchmarkDotNet (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 inspection — ildasm (Windows SDK) or dotnet-ildasm, plus ILSpy for decompilation. [<MethodImpl(MethodImplOptions.AggressiveInlining)>] and inline keyword for control.
Idioms & style
- Naming:
camelCasefor values/functions/modules,PascalCasefor 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);
Resultover exceptions for expected failures; pipe (|>) for left-to-right reading; small modules;inlineonly when SRTP or perf demand it; prefertaskoverasyncfor new code unless you need cancellation correctness or composition. - Expert review focus: file order in
.fsproj(top-down dependencies); accidental .NET nullability leaks (nullfor 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;typerecursion viaand.
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:
dotnetCLI, 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 viaandin same file. nulland 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>.typealiases not opaque —type Email = stringis just an alias; use single-case DUtype Email = Email of stringfor 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 callingfwith a single tuple, not two args; F# functions are curried by default unless declared with tupled parameters (common in .NET interop). printfnis typed —printfn "%d" "hello"is a compile error; use%Ofor any object.asyncvstask—asyncis cold (won’t start until run);taskis hot. Mixing them inside a CE is awkward.do!insideseq { ... }is not allowed; sequences are pull-based.- Discriminated union casing collisions with module/type names; qualify with
Cases.Xif 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
asynchad per-step allocation overhead;task(state-machine-based) is much faster.
Citations
- F# docs: https://learn.microsoft.com/en-us/dotnet/fsharp/
- F# language reference: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/
- F# style guide: https://learn.microsoft.com/en-us/dotnet/fsharp/style-guide/
- F# code formatting: https://learn.microsoft.com/en-us/dotnet/fsharp/style-guide/formatting
- F# language spec: https://fsharp.org/specs/language-spec/
- F# RFCs: https://github.com/fsharp/fslang-design
- What’s new in F# 10: https://learn.microsoft.com/en-us/dotnet/fsharp/whats-new/fsharp-10
- Type providers tutorial: https://learn.microsoft.com/en-us/dotnet/fsharp/tutorials/type-providers/
- Computation expressions: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions
- F# compiler source: https://github.com/dotnet/fsharp
- F# Software Foundation: https://fsharp.org/
- Fable: https://fable.io/
- Fantomas formatter: https://fsprojects.github.io/fantomas/
- FCS: https://fsharp.github.io/fsharp-compiler-docs/
- F#-friendly libraries (community): https://fsharp.org/guides/data-science/