إشعار
يتطلب الوصول إلى هذه الصفحة تخويلاً. يمكنك محاولة تسجيل الدخول أو تغيير الدلائل.
يتطلب الوصول إلى هذه الصفحة تخويلاً. يمكنك محاولة تغيير الدلائل.
Welcome to F# 11! This release helps you keep domain models distinct, reuse generic algorithms, coordinate asynchronous work, and reduce compiler memory use.
- Record spreads reuse fields without forcing storage and application models to share a type.
- Extension members in generic algorithms let existing types share operations without wrappers.
- .NET integration preserves delegate metadata and reuses API documentation.
- Async and task helpers carry cancellation across API boundaries and limit concurrent work.
- Compiler and library improvements reduce repeated analysis and avoid some allocations.
Thank you to the F# community for the proposals, implementations, reviews, and tests behind these changes. Contributor links accompany the features below.
Note
The examples require an F# 11 compiler and FSharp.Core 11 with the features shown. In the .NET 11.0.100 SDK, F# 11.2 features require <LangVersion>preview</LangVersion>; see language settings and planned SDK support. The runtime-async example has additional runtime requirements; an earlier .NET 11 SDK alone might not contain every change.
Record spreads: distinct models without repetitive mappings
To keep storage and application models distinct, record spreads reuse their shared fields without repeated declarations. Here the models share customer details but use different identifier types:
open System
type CustomerId = CustomerId of Guid
type CustomerDetails = { Name: string; PreferredLanguage: string }
type CustomerRow =
{ ...CustomerDetails
Id: string
InternalNote: string }
type Customer =
{ ...CustomerDetails
Id: CustomerId }
...CustomerDetails contributes field declarations. At the boundary, ...row copies values while the conversion parses the identifier:
let toCustomer (row: CustomerRow) =
match Guid.TryParse row.Id with
| true, id -> Ok ({ ...row; Id = CustomerId id }: Customer)
| false, _ -> Error "Invalid customer identifier."
The destination keeps the shared details, replaces Id, and excludes InternalNote. Guid.TryParse, not the spread, validates the identifier; malformed input produces Error.
This addresses the application-layer mapping problem. Value spreads also combine named and anonymous records; type spreads create named records only. Later fields win, and sources run once in order. See Record spread operators for composition examples and shared-evolution risks.
Thanks to @brianrourkeboll for record spreads.
Extension members for generic algorithms, operators, and tuples
To share an algorithm across existing types, F# 11.2 lets extension members satisfy statically resolved type parameter (SRTP) constraints. For example, one transformation can handle lists and options without wrapper types.
Note
In the .NET 11.0.100 SDK, enable <LangVersion>preview</LangVersion> where extensions are resolved and open their module. See library requirements.
Share a transformation across lists and options
With the library's map in scope, the application can clean either representation, change element types, and compose transformations:
let inline cleanNames values =
map (fun (name: string) -> name.Trim().ToUpperInvariant()) values
let batch: string list = cleanNames [ " Ada "; " grace " ]
let optional: string option = cleanNames (Some " linus ")
let missing: string option = cleanNames (None: string option)
let lengths: int list = map String.length batch
let nested: int option list = map (map String.length) [ Some "abc"; None ]
printfn "%A; %A; %A" batch optional missing
printfn "%A; %A" lengths nested
// ["ADA"; "GRACE"]; Some "LINUS"; None
// [3; 5]; [Some 3; None]
The adapters in Structural.Adapters use familiar operations, rather than imposing new containers:
type List<'T> with
static member Map(source: 'T list, mapping: 'T -> 'U) : 'U list =
List.map mapping source
type Option<'T> with
static member Map(source: 'T option, mapping: 'T -> 'U) : 'U option =
Option.map mapping source
The shared function in Structural.Algorithms asks the input type for a static Map member:
let inline map (mapping: 'T -> 'U) (source: ^Input) : ^Output =
(^Input :
(static member Map: ^Input * ('T -> 'U) -> ^Output)
(source, mapping))
^Input and ^Output are statically resolved types. At each concrete call, the compiler selects the member with the required signature. The extensions don't add an interface implementation. The complete application opens both modules.
Operators, context, and output types
The same mechanism supports tuple operators and context propagation and output-directed construction. Native tuple headers need the F# 11.2 feature enabled at their declarations as well as at extension-solving call sites.
A separate addition, AllowOverloadOnReturnType, lets one factory declare overloads distinguished by their returns:
type Inject =
[<AllowOverloadOnReturnType>]
static member Pure(value: 'T) : 'T list = [value]
[<AllowOverloadOnReturnType>]
static member Pure(value: 'T) : 'T option = Some value
let batch: string list = Inject.Pure "ready"
let optional: string option = Inject.Pure "ready"
// ["ready"] and Some "ready"
This Core 11 attribute doesn't require preview. The result annotation selects an overload; without it the call remains ambiguous. See Select by return type.
Thanks to @gusty for extending generic programming support.
.NET integration and the development experience
Direct delegates preserve the handler's real contract
To expose handler names and binding attributes to frameworks, F# 11 can target a compatible named function directly instead of a wrapper. ASP.NET Core Minimal APIs can then read the route, service, and header contract:
open System
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Mvc
open Microsoft.Extensions.DependencyInjection
type ShipmentStore() =
member _.Status trackingId =
if trackingId = "SHP-1042" then "Dispatched" else "Unknown"
let status
(trackingId: string)
([<FromServices>] store: ShipmentStore)
([<FromHeader(Name = "X-Customer")>] customer: string) =
$"{customer}: {trackingId} is {store.Status trackingId}"
let handler = Func<string, ShipmentStore, string, string>(status)
See the runnable project and construction/compatibility rules. Captures and argument adaptation can still require wrappers.
Thanks to @kerams for direct delegates.
Reuse XML documentation
To maintain shared API remarks once, place them in shared.xml:
<docs>
<tracking>
<remarks>An unrecognized tracking identifier has status Unknown.</remarks>
</tracking>
</docs>
With GenerateDocumentationFile enabled, F# 11 expands <include> into the emitted member entries:
/// <summary>Find the latest status of a shipment.</summary>
/// <include file="shared.xml" path="/docs/tracking/*"/>
let status trackingId =
if trackingId = "SHP-1042" then "Dispatched" else "Unknown"
/// <summary>Find the latest statuses of several shipments.</summary>
/// <include file="shared.xml" path="/docs/tracking/*"/>
let statuses trackingIds =
trackingIds |> List.map (fun id -> id, status id)
The include implementation updates both descriptions from one fragment. Separately, the tooling resolver exposes base descriptions for <inheritdoc/>; the compiler leaves that tag for documentation consumers. See XML documentation for examples and resolution rules.
Thanks to @T-Gro for shared XML documentation in FSharp.Core.
Debug inline functions in non-optimized builds
To inspect inline helper arguments, set a breakpoint on let total in a non-optimized build and step into addFee. F# 11 now prefers callable methods in these builds:
let inline addFee (fee: decimal) (amount: decimal) =
let total = amount + fee
total
let invoiceTotal = addFee 2m 40m
printfn "%M" invoiceTotal
// 42
Optimized builds retain inlining. Some constructs must still expand. See Debug inline functions for those limits and the --always-inline control.
Async and task coordination
To connect F# workflows with task-based APIs, FSharp.Core 11 adds cancellation-aware bridges, bounded batches, and result transformations.
Bound outstanding I/O, not just the result collection
A large batch of reads can start more work than an application should have outstanding. To decide when task-based work begins, a coordinator needs factories, not tasks that have already started:
open System.IO
open System.Threading
let readFiles (paths: seq<string>) (ct: CancellationToken) =
paths
|> Seq.map (fun path ->
fun token -> File.ReadAllTextAsync(path, token))
|> Task.parallelLimit 4 ct
At most four reads run concurrently; results retain input order. A failure requests sibling cancellation through the supplied tokens. This limits concurrency, not requests per second. See Async and task functions for sequential and unit-result variants.
Keep cancellation and the underlying exception across the boundary
When a workflow starts external work, its token should travel with that work. Async.StartTaskImmediate passes the running workflow's token to the task factory and waits for completion:
open System.IO
let tryReadFile path =
async {
try
let! contents =
Async.StartTaskImmediate(fun ct -> File.ReadAllTextAsync(path, ct))
return Some contents
with :? FileNotFoundException ->
return None
}
A single task failure reaches this handler as the underlying FileNotFoundException. For an already-created task or value task, use Async.Await; the older Async.AwaitTask retains its exception-wrapping behavior.
For task-based callers, Task.startAsyncImmediate token workflow provides the reverse bridge. It starts on the calling thread until the first asynchronous suspension.
Use matching helpers for short transformations
To transform one result without a full computation expression, use the matching helpers in Microsoft.FSharp.Control. For example, read a file, obtain its length, and return an ordinary failure as Error:
open System.IO
open System.Threading
let fileLength (path: string) (ct: CancellationToken) =
File.ReadAllTextAsync(path, ct)
|> Task.map String.length
|> Task.catch
A file containing Report 1 gives Ok 8; a missing file gives an Error containing FileNotFoundException. Cancellation still propagates. Async and ValueTask offer matching map, bind, and error helpers; see Async and task functions. Use a computation expression for branches or several dependent values.
ValueTask helpers require Core's netstandard2.1 or net10.0 build and single consumption.
Thanks to @bartelink for async/task API contributions.
Reraise after asynchronous failure reporting
Operational logging should add information without changing what the caller catches or hiding where the failure began. Computation-expression handlers can now use reraise () even after they await the logger:
Note
Computation-expression rethrow is an F# 11.2 feature. In the .NET 11.0.100 SDK, enable <LangVersion>preview</LangVersion>.
open System
open System.IO
open System.Threading.Tasks
let readReport (logFailure: exn -> Task) path =
task {
try
return! File.ReadAllTextAsync(path)
with :? IOException as error ->
do! logFailure error
return reraise ()
}
After successful logging, the same exception and original throw-site information propagate. A logger failure can replace the original. This addresses the async observability request without runtime async. See Rethrow from a computation expression.
Thanks to @xperiandri for this support.
FSharp.Core can build on modern .NET
To use modern APIs without dropping older targets, FSharp.Core adds net10.0 alongside netstandard2.0 and netstandard2.1. NuGet selects the compatible build. Updated tool packaging also enables task-based asynchronous disposal in F# Interactive, as in compiled projects. See FSharp.Core target frameworks.
Thanks to @T-Gro for modern Core targeting and SDK tool packaging.
Runtime async for builder authors
Builder authors can let the runtime suspend and resume marked methods instead of using compiler-generated state machines. An experimental library-defined builder exposes the mechanism as a computation expression:
let readManifest (path: string) =
runtimeTask {
let! text = System.IO.File.ReadAllTextAsync path
return text.Length
}
Note
This experimental builder uses an F# 11.2 feature where its inline implementation is defined or expanded; in the .NET 11.0.100 SDK, enable preview. It also requires Core 11's net10.0 build and references and a runtime with runtime-async support. The verified runtime/reference combination here is .NET 11 RC1. Targeting .NET 10 alone isn't sufficient. See Runtime async experiments.
FSharp.Core doesn't ship this sample as a general-purpose builder. Ordinary task and async code doesn't automatically switch to it, and callers of an already-compiled ordinary task-returning API don't inherently need preview.
Thanks to @majocha for runtime async.
Native AOT-friendly text and two-dimensional arrays
To support Native AOT, F# 11 removes reflection-based formatting and dynamic allocation requirements from more common text and array operations.
Print values and build strings without a redundant format layer
When you already have a string, you don't need a "%s" format just to write it. New Core functions print and printn accept values through F#'s string conversion; printn appends a newline:
let report (processed: int) (total: decimal) (elapsed: TimeSpan) : string =
$"{processed} items, total {total:F2}, elapsed {elapsed.TotalMilliseconds:F0} ms"
printn (report 12 145.50m (TimeSpan.FromMilliseconds 250.0))
// 12 items, total 145.50, elapsed 250 ms
String interpolation uses direct conversion and String.Concat in more cases. .NET-format holes such as {total:F2} use invariant formatting and box the value; %A and precision-bearing printf forms can still need printf formatting. Select an explicit culture for localized output.
With --reflectionfree, records and unions also gain generated ToString output. See Interpolation and Native AOT for culture examples and supported paths.
Thanks to @bbatsov for the print functions and @charlesroddie for the interpolation implementation.
Keep ordinary zero-based grid operations
A zero-based grid shouldn't require a replacement Array2D module just to avoid a dynamic-code allocation path. Updated Core implementations support familiar initialization and transformation:
let grid = Array2D.init 3 4 (fun row column -> row * 4 + column)
let doubled = grid |> Array2D.map (fun value -> value * 2)
let snapshot = Array2D.copy doubled
printn $"{snapshot.GetLength(0)} x {snapshot.GetLength(1)}; last = {snapshot[2, 3]}"
// 3 x 4; last = 22
Array2D.create, init, and rebase use the zero-based allocation path across Core builds. Bounds-preserving map, mapi, and copy can use a newer .NET allocation API in the net10.0 build; the .NET Standard builds retain their fallback. Non-zero-based arrays aren't deprecated, but their native allocation restrictions remain.
See Two-dimensional arrays and Native AOT for allocation paths and remaining restrictions. Check the complete application's publish diagnostics.
Thanks to @T-Gro for the Array2D improvements.
Less retained memory and less repeated work
F# 11 reduces repeated compiler analysis and some application allocations. These affect different workloads.
Share referenced libraries across projects
To avoid repeated copies of shared binary dependencies, the language service can share eligible imported library graphs across projects.
In the implementing PR's retained-memory experiment, the ten-project ReSharper.FSharp solution fell from 1,547.2 MB to 1,002.2 MB with sharing enabled. That is an off/on measurement for that workload, not a general F# 10-to-11 reduction. Solutions dominated by project references rather than shared binary references can see much smaller gains.
Metadata reading and inline specialization also reduce temporary allocations, a separate metric. See Tooling performance for scope and controls.
Thanks to @auduchinok for shared library imports.
Compile independent work in parallel by default
Parallel compilation is now enabled by default, with deterministic output and source-order rules preserved. Overload caching and guarded-pattern improvements avoid other repeated work. Visual Studio/FCS also reduce unchanged-document analysis and bound concurrent checks; see Tooling performance.
Avoid callback and empty-result allocation overhead
Selected List and Array functions now inline callbacks. For your own inline helpers, OptimizeClosureIfNotInlined adapts an opaque curried callback once:
let inline foldItems
([<InlineIfLambda; OptimizeClosureIfNotInlined>] folder)
state
(items: 'T[]) =
let mutable total = state
for item in items do
total <- folder total item
total
Other allocations can remain. See Optimize curried callbacks for requirements.
Empty results are another common case of successful work. FSharpLint's motivating workload repeatedly combined rules that found no warnings. Core now reuses shared empty arrays in more operations:
let checkTitle (title: string) =
if String.IsNullOrWhiteSpace title then [|"A title is required."|]
else Array.empty
let warnings = ["Introduction"; "Installation"] |> List.toArray |> Array.collect checkTitle
let sharedEmpty = obj.ReferenceEquals(warnings, Array.empty<string>)
// true: no new empty result array
The change removes redundant empty-array objects, not every allocation in the pipeline. Don't rely on separate empty results having distinct reference identities.
Thanks to @T-Gro for inline collection callbacks and shared empty arrays.
More precise API calls and everyday conveniences
Require names when generated parameter order can change
To protect generated APIs from silent parameter-order changes, an author can require named arguments:
Note
Enforcement is an F# 11.2 feature; in the .NET 11.0.100 SDK, enable preview. An available System.Diagnostics.CodeAnalysis.RequireNamedArgumentsAttribute definition is also required. The complete sample supplies a compatibility definition for references that lack it.
open System.Diagnostics.CodeAnalysis
type Query =
[<RequireNamedArguments>]
static member Execute(numerator: decimal, denominator: decimal) =
numerator / denominator
let ratio = Query.Execute(numerator = 3m, denominator = 4m)
// 0.75M; Query.Execute(3m, 4m) is rejected.
Reordering the declaration preserves the named call's result, 0.75M; a positional call is rejected. For the generated-API use case, the provider must emit the annotation.
Recognize an already-wrapped generic argument
To avoid wrapping an existing task as a plain result value, a new last-resort overload rule prefers Task<'T> over bare 'T:
Note
The most-concrete tiebreaker and overload-priority behavior are F# 11.2 features; in the .NET 11.0.100 SDK, enable preview.
open System.Threading.Tasks
let pending = Task.FromResult 42
let wrapped = ValueTask<_>(pending)
let answer: int = wrapped.Result
// ValueTask<int>, not ValueTask<Task<int>>
The result is ValueTask<int>, not ValueTask<Task<int>>. See Overload selection for the rule's limits.
Prefer a new overload without removing the old entry point
A library might keep an old diagnostic method for existing binaries while adding an overload that includes the caller's source location. OverloadResolutionPriorityAttribute makes that preference explicit for newly compiled callers:
open System.Runtime.CompilerServices
type Check =
static member That(condition: bool) =
if condition then "OK" else "Check failed."
[<OverloadResolutionPriority(1)>]
static member That(condition: bool, [<CallerLineNumber>] ?line: int) =
if condition then "OK" else $"Check failed at line {defaultArg line 0}."
let stock = 0
let diagnostic = Check.That(stock > 0)
// The diagnostic includes the caller's source line.
The unchanged call selects the line-aware overload. See Overload selection for scope and compatibility rules.
Use .NET's conditional nullability promises
If your file path is non-null, a Path API's nullable result shouldn't force a check that its own contract rules out. F# 11 consumes the existing .NET NotNullIfNotNull promise: a non-null named input implies a non-null result.
open System.IO
let fileName: string = Path.GetFileName("reports/annual.csv")
let extension: string = Path.GetExtension(fileName)
printn (extension.ToUpperInvariant())
// .CSV
let fileNameOf (path: string | null) : string | null =
Path.GetFileName path
Enable <Nullable>enable</Nullable>. Nullable inputs still need nullable handling; see Conditional return contracts. For unconstrained C# T? APIs, use the separate Unchecked.withNull helper.
Thanks to @kerams for conditional nullability support.
Smaller language improvements
To call a record type with .NET constructor syntax, use F# 11.2 record constructors:
type Person = { Name: string; Age: int }
let person = Person(Name = "Ada", Age = 36)
Named arguments avoid dependence on field order. Thanks to @charlesroddie for this feature.
For conditional source, #elif avoids nested #if groups:
#if EU_WAREHOUSE
let dispatchTimeZone = "Europe/Paris"
#elif US_WAREHOUSE
let dispatchTimeZone = "America/New_York"
#else
let dispatchTimeZone = "UTC"
#endif
System.Console.WriteLine(dispatchTimeZone)
The first matching branch wins. The sample project defines EU_WAREHOUSE and prints Europe/Paris; with neither symbol defined, the output is UTC.
For related tooling and interop cases:
- To identify your interactive toolchain, use
#version. - To share source with an external host, F# accepts
#:host-directive lines without interpreting them as package or build instructions. - To avoid redundant interface declarations, use a derived interface's default implementation.
- To read inherited state inside an ordinary closure, use the new support for protected base fields.
Before upgrading
Keep these consequential changes in mind when upgrading:
| Affected code | Action |
|---|---|
Libraries with .fsi files |
Copy enforced contract attributes to signatures; omissions are now FS3888 errors. |
| Extension-backed inline libraries | Review inferred signatures and producer/consumer requirements. F# 11.2 inference can retain more general constraints. |
| Instance-method delegates | Validate receivers before direct construction, when null-receiver failures now occur. |
| Localized interpolated output | .NET-format holes use invariant culture. Choose an explicit culture when localized output is intended. |
Custom builders with final do! |
Supply binding and unit completion; final do! no longer selects final-forwarding members. |
Unfinished let! |
Add the following computation body; an incomplete binding now has a more direct diagnostic. |
| Older language settings | Supported numbered modes start at F# 8; non-strict indentation mode is removed. See Language version and indentation. |
| Enum bitwise operations | Use integral-backed enums; invalid backing types are rejected. |