Today, we’re incredibly pleased to announce general availability of F# 4.5.
This post will walk through the changes in F# 4.5 (just like the preview post), then show some updates to F# tooling, and finally talk a bit about where what we’re focusing on for future F# versions.
Get started
F# 4.5 can be acquired in two ways:
If you are not using Windows, never fear! Visual Studio for Mac and Visual Studio Code with Ionide support F# 4.5 if you have the .NET Core SDK installed.
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 had 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 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:
What this means for Windows build servers is that the F# Compiler SDK MSI singleton is no longer supported. If you are using it, we recommend the following choices:
- The Visual Studio Build Tools SKU (look under Tools for Visual Studio).
- The .NET Core SDK if you are using .NET SDK-based projects.
We also do not recommend installing the full Visual Studio IDE on a build server if you can avoid it, but if it is needed for your environment, it will install everything you need for F# 4.5 builds.
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.
Development process
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 in the following places:
We are incredibly grateful to the F# community and look forward to their involvement as F# continues to evolve.
F# tooling updates
F# 4.5 also released alongside Visual Studio 2017 version 15.8, and with it came significant improvements to F# language tooling. Because F# is cross-platform, many of the changes made are available in all F# tooling, not just that in Visual Studio.
Performance improvements
Performance improvements to F# and F# tools were done in this release, some of which had very strong community involvement. Some of these improvements include:
- Removing ~2.2% of allocations in the F# compiler when used in tooling scenarios.
- Comparison for bools (used throughout tooling) now uses fast generic comparison, contributed by Vasily Kirichenko.
- Significant IntelliSense performance improvements for .NET SDK projects in Visual Studio, including when multitargeting, by eliminating places where redundant work was performed in the editor.
- IntelliSense for very large F# files (10k+ lines of code) has been significantly improved to be roughly twice as fast, thanks to a community effort led by Vasily Kirichenko, Steffen Forkmann, and Gauthier Segay.
Although the performance improvements are not quite as dramatic as they were with the VS 15.7 update, you should notice snappier language features for .NET SDK projects and very large files.
New Visual Studio features
In addition to tooling-agnostic changes, Visual Studio 2017 version 15.8 also has some great new features.
Automatic brace completion
There is now automatic brace completion for ""
, (**)
, ()
, []
, [||]
, {}
, and [<>]
pairs. They are “smart” in that typing the completed character (e.g., ]
) will not produce a redundant, unmatched character.
ctrl+Click to Go to Definition
This one is pretty straightforward. Hold the ctrl key to hover over an F# symbol to click on it as if it were a link.
This can be configured to be a different key or use the Peek at Definition window in Tools > Options > Text Editor
F# type signatures in CodeLens adornments (experimental)
Inspired by Ionide, F# Code Lens is a feature implemented by Victor Peter Rouven Müller to show F# type signatures as CodeLens that you can click to go to definition. It is available under Tools > Options > Text Editor > F# > CodeLens (Experimental).
We’re quite excited about how this feature will evolve in the future, as it is a wonderful tool for those who are learning F#. We encourage enthusiastic individuals to help contributing to this feature in our repository.
F# evolution
Although our primary focus has been on F# 4.5 and releasing tooling for Visual Studio 2017 version 15.8, we’ve been doing some thinking about what comes in future F# releases.
Our stance on F# evolution is that it should be fairly predictable and steady moving forward, with a release every year or half-year. What constitutes a release is difficult to plan, especially because some features that seem easy can prove incredibly hard, while some features that seem hard can prove much easier than expected. Additionally, the F# community always has great contributions that we want to ship to the rest of the world, such as the match!
feature in F# 4.5, which affects how we think about a bundle of improvements.
You can see which language suggestions we view as a priority to implement in the F# language suggestions repo. Of these, we are currently focused on the following bigger items:
- Distinguishing nullable and non-nullable reference types, with backwards-compatible compile-time enforced null-safety.
- Anonymous Record types.
- A
task { }
computation expression.
Many things in the proposed priority list are smaller in scope, but the previously-mentioned items will take significant time to implement correctly. We look forward to deep community engagements, and welcome motivated compiler hackers to try their hand at writing an RFC for a feature and proposing an implementation.
Lastly, one of the big efforts happening for .NET is ML.NET. F# is used for data science and analytical workloads by many people, where they do things such as use F# types and combinators to manipulate data before it is input to an algorithm, or use Units of Measure to enforce correctness for numerical values. We feel that this aspect of F# usage should be amplified a bit, especially given the rising importance (and perhaps eventual ubiquity) of machine learning in software systems.
We have spent some time working out how F# can better position itself in the machine learning space without sacrificing the sense of simplicity and elegance it can bring to other domains such as web and cloud development. One manifestation of this is adding support for F# records (with the CLIMutable
attribute) to ML.NET 0.4. Although this isn’t a change to F# itself, it does help people using this library use it in a more idiomatic way. We have also treated existing projects, such as DiffSharp, Deedle, and FsLab as the starting point for this work. The initial goal is to help the F# community to have a coherent way to use F# for machine learning with various libraries and tools. Community and open source is essential to success with data science and machine learning, regardless of language or platform, so we’ve consciously started there.
Although our primary focus for F# and ML has been more foundational and broad in scope, as use cases emerge they may justify language changes that are generally applicable to F# but really shine when used for ML and analytical workloads. These will be considered carefully and evaluated based on how useful they can be generally – shoehorning of “machine learning features” is not a goal. As always, we welcome suggestions and feedback in the F# language suggestions repository if you are interested in helping F# evolve with this as inspiration.
Cheers, and happy F# coding!