We’re very excited to announce that we’ll be shipping a new language version of F# soon. The version will be F# 4.5.
F# 4.5 has been developed entirely via an open RFC (requests for comments) process, with significant contributions from the community, especially in feature discussions and demonstrating use cases. You can view all RFCs that correspond with this release:
This post will talk a little bit about some of the features in this new language version.
Get started
First, install:
- A preview of the .NET SDK 2.1.400 to ensure you have the latest bits for for F# in .NET Core
- Visual Studio 2017 update 15.8 Preview 5 if you are on Windows
If you create a .NET desktop F# project in Visual Studio (from the F# desktop development component), then you will need to update your FSharp.Core package to 4.5.1 with the NuGet UI.
When .NET Core SDK 2.1.400 and Visual Studio 2017 version 15.8 are released, the referenced FSharp.Core will be 4.5.1 for all new projects and you will not need to perform this second step.
Versioning Alignment
The first thing you may notice about F# 4.5 is that it’s higher than F# 4.1. But you may not have expected it to be four decimal places higher! The following table details this change for F# 4.5:
F# language version | FSharp.Core binary version | FSharp.Core NuGet package | |
---|---|---|---|
Old world | F# 4.1 | 4.4.1.0 | 4.x.y, where x >= 2 for .NET Standard support |
New world | F# 4.5 | 4.5.x.0 | 4.5.x |
There is a good reason for this change. As you can see, prior to F# 4.5, each item has a different version! The reasons for this are historical, but the end result is that it has been horribly confusing for F# users for a long time. So, we decided to clear it up.
From here on out, the major and minor versions for these three assets will be synced, as per the RFC we wrote detailing this change.
Side-by-side F# compilers deployed by Visual Studio
In previous versions of Visual Studio, you may have noticed that the F# Compiler SDK was bumped from 4.1 to 10.1. This is because the F# compiler evolves more rapidly than the language, often to fix bugs or improve performance. In the past, this was effectively ignored, with the compiler SDK still containing the same version number as the language version. This inaccurate representation of artifacts on your machine drove an effort to separate the versioning of the compiler and tools separately from the language they implement. As per the RFC detailing this change, the F# compiler and tools will use semantic versioning.
Additionally, the F# compiler SDK used to install as a singleton on your machine. If you had multiple side-by-side Visual Studio 2017 versions installed on your machine, the tools for all installations would pick up the same F# compiler, regardless of if they were ever intended to use that compiler. This means that something like using both release and preview version of Visual Studio could silently opt-in your release build of Visual Studio to use preview bits of F#! This resulted in multiple issues where users were confused about the behavior on their machine.
For F# 4.5, the compiler SDK version will be 10.2. When installed with Visual Studio, this will be fully side-by-side with that version of Visual Studio. Additional Visual Studio installations will not pick this higher compiler SDK version.
Accounting for this change on Windows build servers:
You may be doing one of the following things to install F# on a Windows build server:
- Installing the full Visual Studio IDE
- Installing the F# Compiler SDK MSI
Neither of these options have been recommended for some time, but are still available with F# 4.1.
For using F# 4.5 in a Windows build server, we recommend (in order of preference), Using the .NET SDK, the FSharp.Compiler.Tools package, or the Visual Studio Build Tools SKU. This change will be documented in the official F# docs and the F# Software Foundation guides page by the time F# 4.5 is out of preview.
Span support
The largest piece of F# 4.5 is a feature set aligned with the new Span feature in .NET Core 2.1. The F# feature set is comprised of:
- The
voidptr
type. - The
NativePtr.ofVoidPtr
andNativePtr.toVoidPtr
functions in FSharp.Core. - The
inref<'T>
andoutref<'T>
types, which are readonly and write-only versions ofbyref<'T>
, respectively. - The ability to produce
IsByRefLike
structs (examples of such structs:Span<'T>
andReadOnlySpan<'T>
). - The ability to produce
IsReadOnly
structs. - Implicit de-reference of
byref<'T>
andinref<'T>
returns from functions and methods. - The ability to write extension methods on
byref<'T>
,inref<'T>
, andoutref<'T>
(note: not optional type extensions). - Comprehensive safety checks to prevent unsoundness in your code.
The main goals for this feature set are:
- Offer ways to interoperate with and product high-performance code in F#.
- Full parity with .NET Core performance innovations.
- Better code generation, especially for byref-like constructs.
What this boils down into is a feature set that allows for safe use of performance-oriented constructs in a very restrictive manner. When programming with these features, you will find that they are far more restrictive than you might initially anticipate. For example, you cannot define an F# record type that has a Span inside of it. This is because a Span is a “byref-like” type, and byref-like types can only contained in other byref-like types. Allowing such a thing would result in unsound F# code that would fail at runtime! Because of this, we implement strict safety checks in the F# compiler to prevent you from writing code that is unsound.
If you’ve been following along with the Span<'T>
and ref
work in C# 7.3, a rough syntax guide is as follows:
C# | F# |
---|---|
out int arg |
arg: byref<int> |
out int arg |
arg: outref<int> |
in int arg |
arg: inref<int> |
ref readonly int |
Inferred or arg: inref<int> |
ref expr |
&expr |
The following sample shows a few ways you can use Span<'T>
with F#:
Safety rules for byrefs
As previously mentioned, byref
s and byref
-like structs are quite restrictive in how they can be used. This is because the goal of this feature set is to make low-level code in the style of pointer manipulation safe and predictable. Doing so is only possible by restricting usage of certain types to appropriate contexts and performing scope analysis on your code to ensure soundness.
A quick summary of some of the safety rules:
- A
let
-bound value cannot have its reference escape the scope it was defined in. byref
-like structs cannot be instance or static members of a class or normal struct.byref
-like structs cannot by captured by any closure construct.byref
-like structs cannot be used as a generic type parameter.
As a reminder, Span<'T>
and ReadOnlySpan<'T>
are byref
-like structs and are subject to these rules.
Bug fixes that are not backwards compatible
There are two bugs fixes as a part of this feature set that are not backwards compatible with F# 4.1 code that deals with consuming C# 7.x ref returns and performs “evil struct replacement”.
Implicit dereference of byref-like return values
F# 4.1 introduced the ability for F# to consume byref returns. This was done strictly for interoperation with C# ref
returns, and F# could not produce such a return from F# constructs.
However, in F# 4.1, these values were not implicitly dereferenced in F# code, unlike how they were in equivalent C#. This meant that if you attempted to translate C# code that consumed a ref
return into equivalent F#, you’d find that the type you got back from the call was a pointer rather than a value.
Starting with F# 4.5, this value is now implicitly dereferenced in F# code. In addition to bringing this feature set in line with C# behavior, this allows for assignment to byref
returns from F# functions, methods, and properties as one would expect if they had learned about this feature with C# 7 and higher.
To avoid the implicit dereference, simply apply the &
operator to the value to make it a byref
.
Disabling evil struct replacement on immutable structs
F# 4.1 (and lower) had a bug in the language where an immutable struct could define a method that completely replaced itself when called. This so-called “evil struct replacement” behavior is considered a bug now that F# has a way to represent ReadOnly
structs. The this
pointer on a struct will now be an inref<MyStruct>
, and an attempt to modify the this
pointer will now emit an error.
You can learn more about the full design and behavior of this feature set in the RFC.
New keyword: match!
Computation Expressions now support the `match!` keyword, shortening somewhat common boilerplate existing in lots of code today.
This F# 4.1 code:
Can now be written with match!
in F# 4.5:
This feature was contributed entirely by John Wostenberg in the F# OSS community. Thanks, John!
Relaxed upcast requirements with yield in F# sequence, list and array expressions
A previous requirement to upcast to a supertype when using yield
used to be required in F# sequence, list, and array expressions. This restriction was already unnecessary for these expressions since F# 3.1 when not using yield
, so this makes things more consistent with existing behavior.
Relaxed indentation rules for list and array expressions
Since F# 2.0, expressions delimited by ‘}’ as an ending token would allow “undentation”. However, this was not extended to array and list expressions, thus resulting in confusing warnings for code like this:
The solution would be to insert a new line for the named argument and indent it one scope, which is unintuitive. This has now been relaxed, and the confusing warning is no more. This is especially helpful when doing reactive UI programming with a library such as Elmish.
F# enumeration cases emitted as public
To help with profiling tools, we now emit F# enumeration cases as public under all circumstances. This makes it easier to analyze the results of running performance tools on F# code, where the label name holds more semantic information than the backing integer value. This is also aligned with how C# emits enumerations.
Better async stack traces
Starting with F# 4.5 and FSharp.Core 4.5.0, stack traces for async computation expressions:
- Reported line numbers now correspond to the failing user code
- Non-user code is no longer emitted
For example, consider the following DSL and its usage with an FSharp.Core version prior to 4.5.0:
Note that both the f1
and f2
functions are called twice. When you look at the result of this in F# Interactive, you’ll notice that stack traces will never list names or line numbers that refer to the actual invocation of these functions! Instead, they will refer to the closures that perform the call:
This was confusing in F# async code prior to F# 4.5, which made diagnosing problems with async code difficult in large codebases.
With FSharp.Core 4.5.0, we selectively inline certain members so that the closures become part of user code, while also selectively hiding certain implementation details in relevant parts of FSharp.Core from the debugger so that they don’t accidentally muddy up stack traces.
The result is that names and line numbers that correspond to actual user code will now be present in stack traces.
To demonstrate this, we can apply this technique (with some additional modifications) to the previously-mentioned DSL:
When ran again in F# Interactive, the printed stack trace now shows names and line numbers that correspond to user calls to functions, not the underlying closures:
As mentioned in the RFC for this feature, there are other problems inherent to the space, and other solutions that may be pursued in a future F# version.
Additional FSharp.Core improvements
In addition to the improved Async stack traces, there were a small number of improvements to FSharp.Core.
Map.TryGetValue
(RFC)ValueOption<'T>
(RFC)FuncConvert.FromFunc
andFuncConvert.FromAction
APIs to enable acceptingFunc<’A, ‘B>
andAction<’A, ‘B>
instances from C# code. (RFC)
The following F# code demonstrates the usage of the first two:
The FuncConvert API additions aren’t that useful for F#-only code, but they do help with C# to F# interoperability, allowing the use of “modern” C# constructs like Action
and Func
to convert into F# functions.
The road to official release
This preview is very, very stable. In fact, after extensive testing, we feel that it’s stable enough for us to consider it a proper release, but due to the timing of the .NET SDK and Visual Studio releases, we’re releasing it now as a preview. Soon, when Visual Studio 2017 update 15.8 and the corresponding .NET Core 2.1 SDK update release, we will declare F# 4.5 as fully released and it will be fully included in both places.
Cheers, and happy coding!