F# 10 Ships: Scoped Warnings, ValueOption Parameters, and the Quest for Clarity
Microsoft has released F# 10 with .NET 10 and Visual Studio 2026, delivering a refinement release focused on ergonomics, consistency, and performance. While not a revolutionary update, the improvements address real pain points that F# developers encounter daily.
The Core Insight
F# 10’s philosophy is clear: make everyday code more legible and robust. Rather than introducing flashy new paradigms, this release polishes rough edges that have accumulated over time. The most impactful changes target areas where F# syntax forced developers into awkward workarounds or created inconsistencies with the rest of the language.
The headline features include scoped warning suppression (finally!), struct-backed optional parameters for performance-critical code, and smoother computation expression syntax. Under the hood, a new type subsumption cache improves IDE responsiveness in complex projects.
Why This Matters
Scoped Warning Suppression (#warnon and #nowarn)
F# developers have long struggled with a binary choice: suppress a warning for an entire file or live with noise on specific lines. Now you can bracket exactly the code you want to suppress:
#nowarn 25
let f (Some x) = // FS0025 suppressed here only
#warnon 25
This is particularly valuable for pattern matching on known-safe partial patterns, a common scenario in production code.
ValueOption for Optional Parameters
Performance-sensitive code can now use struct-backed ValueOption<'T> instead of the heap-allocated option type for optional parameters:
static member M([<Struct>] ?x : string) =
match x with
| ValueSome v -> printfn "ValueSome %s" v
| ValueNone -> printfn "ValueNone"
This eliminates GC pressure in hot paths where optional parameters are frequently absent.
Cleaner Computation Expression Syntax
Type annotations on let!, use!, and and! bindings no longer require parentheses:
// Before F# 10
async {
let! (a: int) = fetchA()
and! (b: int) = fetchB()
}
// F# 10
async {
let! a: int = fetchA()
and! b: int = fetchB()
}
A small change, but it reduces visual noise in async-heavy codebases.
Key Takeaways
Breaking changes exist: The warning directive cleanup may affect existing code. Multiline warn directives, whitespace between
#andnowarn, and certain string formats are no longer allowed.Script behavior changed:
#nowarndirectives in scripts now scope to their file rather than the entire compilation—matching.fsfile behavior.Access modifiers on auto-properties: You can now create publicly readable but privately settable properties without boilerplate:
member val Balance = 0m with public get, private setDeprecation warning for implicit seq: Bare sequence expressions like
{ 1..10 }now generate warnings encouraging the explicitseq { 1..10 }form.Attribute target enforcement: Misapplied attributes (like
[<Fact>]on a property instead of a method) now generate warnings, catching test discovery issues early.
Looking Ahead
F# 10 also brings and! support to task expressions in FSharp.Core, enabling idiomatic concurrent awaiting:
task {
let! a = fetchA()
and! b = fetchB()
return combineAB a b
}
This matches the syntax F# developers already use with async, reducing cognitive overhead when working with Tasks.
The type subsumption cache represents F#’s ongoing investment in tooling performance. As codebases grow more complex and AI-assisted development creates larger changesets, compiler and IDE responsiveness becomes increasingly critical.
For teams already using F#, the upgrade path is straightforward. For those evaluating functional-first languages on .NET, F# 10 continues the language’s tradition of pragmatic design choices that prioritize developer productivity.
Based on analysis of “Introducing F# 10” from Microsoft DevBlogs