Task support was available for F# 5 through the excellent TaskBuilder.fs and Ply libraries. It should be straightforward to migrate code to the built-in support. However, there are some differences: namespaces and type inference differ slightly between the built-in support and these libraries, and some additional type annotations may be needed. If necessary, you can still use these community libraries with F# 6 if you reference them explicitly and open the correct namespaces in each file.
Using task {…} is very similar to using async {…}. Using task {…} has several advantages over async {…}:
The overhead of task {...} is lower, possibly improving performance in hot code paths where the asynchronous work executes quickly.
Debugging stepping and stack traces for task {…} is better.
Interoperating with .NET packages that expect or produce tasks is easier.
If you’re familiar with async {…}, there are some differences to be aware of:
task {…} immediately executes the task to the first await point.
task {…} does not implicitly propagate a cancellation token.
task {…} does not perform implicit cancellation checks.
task {…} does not support asynchronous tailcalls. This means using return! .. recursively may result in stack overflows if there are no intervening asynchronous waits.
In general, you should consider using task {…} over async {…} in new code if you're interoperating with .NET libraries that use tasks, and if you don't rely on asynchronous code tailcalls or implicit cancellation token propagation. In existing code, you should only switch to task {…} once you have reviewed your code to ensure you are not relying on the previously mentioned characteristics of async {…}.
F# 6 allows the syntax expr[idx] for indexing and slicing collections.
Up to and including F# 5, F# has used expr.[idx] as indexing syntax. Allowing the use of expr[idx] is based on repeated feedback from those learning F# or seeing F# for the first time that the use of dot-notation indexing comes across as an unnecessary divergence from standard industry practice.
This is not a breaking change because by default, no warnings are emitted on the use of expr.[idx]. However, some informational messages that suggest code clarifications are emitted. You can optionally activate further informational messages as well. For example, you can activate an optional informational warning (/warnon:3366) to start reporting uses of the expr.[idx] notation. For more information, see Indexer Notation.
In new code, we recommend the systematic use of expr[idx] as the indexing syntax.
Struct representations for partial active patterns
F# 6 augments the "active patterns" feature with optional struct representations for partial active patterns. This allows you to use an attribute to constrain a partial active pattern to return a value option:
[<return: Struct>]
let (|Int|_|) str =
match System.Int32.TryParse(str) with
| true, int -> ValueSome(int)
| _ -> ValueNone
The use of the attribute is required. At usage sites, code doesn't change. The net result is that allocations are reduced.
Consider the following use of a computation expression builder content:
let mem = new System.IO.MemoryStream("Stream"B)
let content = ContentBuilder()
let ceResult =
content {
body "Name"
body (ArraySegment<_>("Email"B, 0, 5))
body "Password"B 2 4
body "BYTES"B
body mem
body "Description" "of" "content"
}
Here the body custom operation takes a varying number of arguments of different types. This is supported by the implementation of the following builder, which uses overloading:
type Content = ArraySegment<byte> list
type ContentBuilder() =
member _.Run(c: Content) =
let crlf = "\r\n"B
[|for part in List.rev c do
yield! part.Array[part.Offset..(part.Count+part.Offset-1)]
yield! crlf |]
member _.Yield(_) = []
[<CustomOperation("body")>]
member _.Body(c: Content, segment: ArraySegment<byte>) =
segment::c
[<CustomOperation("body")>]
member _.Body(c: Content, bytes: byte[]) =
ArraySegment<byte>(bytes, 0, bytes.Length)::c
[<CustomOperation("body")>]
member _.Body(c: Content, bytes: byte[], offset, count) =
ArraySegment<byte>(bytes, offset, count)::c
[<CustomOperation("body")>]
member _.Body(c: Content, content: System.IO.Stream) =
let mem = new System.IO.MemoryStream()
content.CopyTo(mem)
let bytes = mem.ToArray()
ArraySegment<byte>(bytes, 0, bytes.Length)::c
[<CustomOperation("body")>]
member _.Body(c: Content, [<ParamArray>] contents: string[]) =
List.rev [for c in contents -> let b = Text.Encoding.ASCII.GetBytes c in ArraySegment<_>(b,0,b.Length)] @ c
In F# 6, the right-hand side of an as pattern can now itself be a pattern. This is important when a type test has given a stronger type to an input. For example, consider the following code:
type Pair = Pair of int * int
let analyzeObject (input: obj) =
match input with
| :? (int * int) as (x, y) -> printfn $"A tuple: {x}, {y}"
| :? Pair as Pair (x, y) -> printfn $"A DU: {x}, {y}"
| _ -> printfn "Nope"
let input = box (1, 2)
In each pattern case, the input object is type-tested. The right-hand side of the as pattern is now allowed to be a further pattern, which can itself match the object at the stronger type.
F# 6 removes a number of inconsistencies and limitations in its use of indentation-aware syntax. See RFC FS-1108. This resolves 10 significant issues highlighted by F# users since F# 4.0.
For example, in F# 5 the following code was allowed:
let c = (
printfn "aaaa"
printfn "bbbb"
)
However, the following code was not allowed (it produced a warning):
let c = [
1
2
]
In F# 6, both are allowed. This makes F# simpler and easier to learn. The F# community contributor Hadrian Tang has led the way on this, including remarkable and highly valuable systematic testing of the feature.
F# 6 implements additional implicit upcast conversions. For example, in F# 5 and earlier versions, upcasts were needed for the return expression when implementing a function where the expressions had different subtypes on different branches, even when a type annotation was present. Consider the following F# 5 code:
open System
open System.IO
let findInputSource () : TextReader =
if DateTime.Now.DayOfWeek = DayOfWeek.Monday then
// On Monday a TextReader
Console.In
else
// On other days a StreamReader
File.OpenText("path.txt") :> TextReader
Here the branches of the conditional compute a TextReader and StreamReader respectively, and the upcast was added to make both branches have type StreamReader. In F# 6, these upcasts are now added automatically. This means the code is simpler:
let findInputSource () : TextReader =
if DateTime.Now.DayOfWeek = DayOfWeek.Monday then
// On Monday a TextReader
Console.In
else
// On other days a StreamReader
File.OpenText("path.txt")
You may optionally enable the warning /warnon:3388 to show a warning at every point an additional implicit upcast is used, as described in Optional warnings for implicit conversions.
Implicit integer conversions
In F# 6, 32-bit integers are widened to 64-bit integers when both types are known. For example, consider a typical API shape:
type Tensor(…) =
static member Create(sizes: seq<int64>) = Tensor(…)
In F# 5, integer literals for int64 must be used:
Tensor.Create([100L; 10L; 10L])
or
Tensor.Create([int64 100; int64 10; int64 10])
In F# 6, widening happens automatically for int32 to int64, int32 to nativeint, and int32 to double, when both source and destination type are known during type inference. So in cases such as the previous examples, int32 literals can be used:
Tensor.Create([100; 10; 10])
Despite this change, F# continues to use explicit widening of numeric types in most cases. For example, implicit widening does not apply to other numeric types, such as int8 or int16, or from float32 to float64, or when either source or destination type is unknown. You can also optionally enable the warning /warnon:3389 to show a warning at every point implicit numeric widening is used, as described in Optional warnings for implicit conversions.
First-class support for .NET-style implicit conversions
In F# 6, .NET “op_Implicit” conversions are applied automatically in F# code when calling methods. For example, in F# 5 it was necessary to use XName.op_Implicit when working with .NET APIs for XML:
open System.Xml.Linq
let purchaseOrder = XElement.Load("PurchaseOrder.xml")
let partNos = purchaseOrder.Descendants(XName.op_Implicit "Item")
In F# 6, op_Implicit conversions are applied automatically for argument expressions when types are available for source expression and target type:
open System.Xml.Linq
let purchaseOrder = XElement.Load("PurchaseOrder.xml")
let partNos = purchaseOrder.Descendants("Item")
You may optionally enable the warning /warnon:3395 to show a warning at every point op_Implicit conversions widening is used at method arguments, as described in Optional warnings for implicit conversions.
Note
In the first release of F# 6, this warning number was /warnon:3390. Due to a conflict, the warning number was later updated to /warnon:3395.
Optional warnings for implicit conversions
Type-directed and implicit conversions can interact poorly with type inference and lead to code that's harder to understand. For this reason, some mitigations exist to help ensure this feature is not abused in F# code. First, both source and destination type must be strongly known, with no ambiguity or additional type inference arising. Secondly, opt-in warnings can be activated to report any use of implicit conversions, with one warning on by default:
/warnon:3388 (additional implicit upcast)
/warnon:3389 (implicit numeric widening)
/warnon:3391 (op_Implicit at non-method arguments, on by default)
/warnon:3395 (op_Implicit at method arguments)
If your team wants to ban all uses of implicit conversions, you can also specify /warnaserror:3388, /warnaserror:3389, /warnaserror:3391, and /warnaserror:3395.
Formatting for binary numbers
F# 6 adds the %B pattern to the available format specifiers for binary number formats. Consider the following F# code:
The F# compiler includes an optimizer that performs inlining of code. In F# 6 we've added a new declarative feature that allows code to optionally indicate that, if an argument is determined to be a lambda function, then that argument should itself always be inlined at call sites.
For example, consider the following iterateTwice function to traverse an array:
let inline iterateTwice ([<InlineIfLambda>] action) (array: 'T[]) =
for j = 0 to array.Length-1 do
action array[j]
for j = 0 to array.Length-1 do
action array[j]
If the call site is:
let arr = [| 1.. 100 |]
let mutable sum = 0
arr |> iterateTwice (fun x ->
sum <- sum + x)
Then after inlining and other optimizations, the code becomes:
let arr = [| 1.. 100 |]
let mutable sum = 0
for j = 0 to arr.Length-1 do
sum <- sum + arr[j]
for j = 0 to arr.Length-1 do
sum <- sum + arr[j]
Unlike previous versions of F#, this optimization is applied regardless of the size of the lambda expression involved. This feature can also be used to implement loop unrolling and similar transformations more reliably.
An opt-in warning (/warnon:3517, off by default) can be turned on to indicate places in your code where InlineIfLambda arguments are not bound to lambda expressions at call sites. In normal situations, this warning should not be enabled. However, in certain kinds of high-performance programming, it can be useful to ensure all code is inlined and flattened.
The task {…} support of F# 6 is built on a foundation called resumable codeRFC FS-1087. Resumable code is a technical feature that can be used to build many kinds of high-performance asynchronous and yielding state machines.
Additional collection functions
FSharp.Core 6.0.0 adds five new operations to the core collection functions. These functions are:
List/Array/Seq.insertAt
List/Array/Seq.removeAt
List/Array/Seq.updateAt
List/Array/Seq.insertManyAt
List/Array/Seq.removeManyAt
These functions all perform copy-and-update operations on the corresponding collection type or sequence. This type of operation is a form of a “functional update”. For examples of using these functions, see the corresponding documentation, for example, List.insertAt.
As an example, consider the model, message, and update logic for a simple "Todo List" application written in the Elmish style. Here the user interacts with the application, generating messages, and the update function processes these messages, producing a new model:
type Model =
{ ToDo: string list }
type Message =
| InsertToDo of index: int * what: string
| RemoveToDo of index: int
| LoadedToDos of index: int * what: string list
let update (model: Model) (message: Message) =
match message with
| InsertToDo (index, what) ->
{ model with ToDo = model.ToDo |> List.insertAt index what }
| RemoveToDo index ->
{ model with ToDo = model.ToDo |> List.removeAt index }
| LoadedToDos (index, what) ->
{ model with ToDo = model.ToDo |> List.insertManyAt index what }
With these new functions, the logic is clear and simple and relies only on immutable data.
FSharp.Core 6.0.0 adds new intrinsics to the NativePtr module:
NativePtr.nullPtr
NativePtr.isNullPtr
NativePtr.initBlock
NativePtr.clear
NativePtr.copy
NativePtr.copyBlock
NativePtr.ofILSigPtr
NativePtr.toILSigPtr
As with other functions in NativePtr, these functions are inlined, and their use emits warnings unless /nowarn:9 is used. The use of these functions is restricted to unmanaged types.
Informational warnings for rarely used symbolic operators
F# 6 adds soft guidance that de-normalizes the use of :=, !, incr, and decr in F# 6 and beyond. Using these operators and functions produces informational messages that ask you to replace your code with explicit use of the Value property.
In F# programming, reference cells can be used for heap-allocated mutable registers. While they are occasionally useful, they're rarely needed in modern F# coding, because let mutable can be used instead. The F# core library includes two operators := and ! and two functions incr and decr specifically related to reference calls. The presence of these operators makes reference cells more central to F# programming than they need to be, requiring all F# programmers to know these operators. Further, the ! operator can be easily confused with the not operation in C# and other languages, a potentially subtle source of bugs when translating code.
The rationale for this change is to reduce the number of operators the F# programmer needs to know, and thus simplify F# for beginners.
For example, consider the following F# 5 code:
let r = ref 0
let doSomething() =
printfn "doing something"
r := !r + 1
First, reference cells are rarely needed in modern F# coding, as let mutable can normally be used instead:
let mutable r = 0
let doSomething() =
printfn "doing something"
r <- r + 1
If you use reference cells, F# 6 emits an informational warning asking you to change the last line to r.Value <- r.Value + 1, and linking you to further guidance on the appropriate use of reference cells.
let r = ref 0
let doSomething() =
printfn "doing something"
r.Value <- r.Value + 1
These messages are not warnings; they are "informational messages" shown in the IDE and compiler output. F# remains backwards-compatible.
F# tooling: .NET 6 the default for scripting in Visual Studio
If you open or execute an F# Script (.fsx) in Visual Studio, by default the script will be analyzed and executed using .NET 6 with 64-bit execution. This functionality was in preview in the later releases of Visual Studio 2019 and is now enabled by default.
To enable .NET Framework scripting, select Tools > Options > F# Tools > F# Interactive. Set Use .NET Core Scripting to false, and then restart the F# Interactive window. This setting affects both script editing and script execution. To enable 32-bit execution for .NET Framework scripting, also set 64-bit F# Interactive to false. There is no 32-bit option for .NET Core scripting.
F# tooling: Pin the SDK version of your F# scripts
If you execute a script using dotnet fsi in a directory containing a global.json file with a .NET SDK setting, then the listed version of the .NET SDK will be used to execute and edit the script. This feature has been available in the later versions of F# 5.
For example, assume there's a script in a directory with the following global.json file specifying a .NET SDK version policy:
If you now execute the script using dotnet fsi, from this directory, the SDK version will be respected. This is a powerful feature that lets you "lock down" the SDK used to compile, analyze, and execute your scripts.
If you open and edit your script in Visual Studio and other IDEs, the tooling will respect this setting when analyzing and checking your script. If the SDK is not found, you will need to install it on your development machine.
On Linux and other Unix systems, you can combine this with a shebang to also specify a language version for direct execution of the script. A simple shebang for script.fsx is:
Now the script can be executed directly with script.fsx. You can combine this with a specific, non-default language version like this:
#!/usr/bin/env -S dotnet fsi --langversion:5.0
Note
This setting is ignored by editing tools, which will analyze the script assuming latest language version.
Removing legacy features
Since F# 2.0, some deprecated legacy features have long given warnings. Using these features in F# 6 gives errors unless you explicitly use /langversion:5.0. The features that give errors are:
Multiple generic parameters using a postfix type name, for example (int, int) Dictionary. This becomes an error in F# 6. The standard syntax Dictionary<int,int> should be used instead.
#indent "off". This becomes an error.
x.(expr). This becomes an error.
module M = struct … end . This becomes an error.
Use of inputs *.ml and *.mli. This becomes an error.
Use of (*IF-CAML*) or (*IF-OCAML*). This becomes an error.
Use of land, lor, lxor, lsl, lsr, or asr as infix operators. These are infix keywords in F# because they were infix keywords in OCaml and are not defined in FSharp.Core. Using these keywords will now emit a warning.
The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see our contributor guide.
.NET feedback
.NET is an open source project. Select a link to provide feedback:
F# is an open-source, cross-platform programming language that makes it easy to write succinct, performant, robust, and practical code. It's a general-purpose language that enables you to create many different types of applications like Web API, Desktop, IoT, Gaming and more.