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 thinking about for the next F# version.
Get started
F# 4.5 can be acquired in two ways:
If you are not on Windows, never fear! Visual Studio for Mac and Visual Studio Code with Ionide support F# 4.5 if you have the .NET 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 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 will be synced, as per the RFC we wrote detailing this change.
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.
Side-by-side F# compilers deployed by Visual Studio
The F# compiler SDK used to install to a global location on your machine. Any subsequent installation of Visual Studio would overwrite this location if the versions were the same (e.g., for bug fix releases). Additionally, so as not to make a mess on your machine, Visual Studio would remove an installation that it placed there when uninstalled. This has resulted in a plethora of problems for people over the years. For example, when installing release and preview builds of Visual Studio side-by-side, the preview build could overwrite the release F# compiler, making it so that your release build of Visual Studio was using compiler bits it was never tested against! The solution to these sorts of problems is for Visual Studio to install the F# bits it carries side-by-side.
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 (nor other toolsets) will not pick this higher compiler SDK version.
If you have custom tools that previously used the global installation location, you will need to update them to point to:
C:\Program Files (x86)\Microsoft Visual Studio\2017\sku\Common7\IDE\CommonExtensions\Microsoft\FSharp
Where sku
is the Visual Studio SKU that you use (Community, Professional, or Enterprise). If you are using Preview VS bits, you can replace 2017
with Preview
.
Accounting for this change on Windows build servers:
If you are using the F# Compiler SDK MSI singleton for build servers, this will no longer work with F# 4.5 and is considered deprecated. Instead, use the following:
- Visual Studio Build Tools SKU (looks under Tools for Visual Studio).
- .NET SDK if you are using .NET SDK-style projects.
We do not recommend installing the full Visual Studio IDE on your machine if you can avoid it, but if your builds require it, the IDE will install all necessary bits for building F# 4.5 applications.
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 produce 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# |
---|---|
ref 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:
We are incredibly grateful to the F# community and look forward to their involvement as F# continues to evolve.
F# tools 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 under various 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 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 IntelliSense for .NET SDK projects and very large files.
New Visual Studio features
Visual Studio 2017 version 15.8 also has some great new features.
Automatic brace completion
There is now automatic brace completion for ""
, (**)
, ()
, []
, [||]
, {}
, and [<>]
pairs.
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, a feature implemented by Victor Peter Rouven Müller to show F# type signatures as CodeLens is available under Tools > Options > Text Editor > F# > CodeLens (Experimental). If you click F# types in the signatures, they will invoke Go to Definition.
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 in the contribution 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.
Although we don’t have a roadmap in place, you can see which language suggestions we view as a priority to implement. Of these, we are currently focused on the following bigger items:
- Nullability and compile-time enforced null-safety for reference types.
- Anonymous Record types.
- A
task { }
computation expression.
Many things in the proposed priority list are smaller in scope, but the previously mentioned list 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. Many F# users use F# for data science and analytical workloads, using F# types and combinators to manipulate data, using Units of Measure to enforce correctness for numerical values, Type Providers to ingest data, and more.
We have spent some time working out how F# can better position itself in the machine learning space. One manifestation of this is adding support for F# records (with the CLIMutable
attribute) to ML.NET 0.4. We are also working with prior art in this work, such as DiffSharp, Deedle, and FsLab as a starting point. Our initial goal is to help the F# community have a coherent way to use F# for machine learning with various libraries and tools. Community and open source is essential to success in data science and machine learning, regardless of language or platform, so we’ve consciously chosen to start there.
Although our primary focus for F# and ML has been more foundational and broad, as use cases emerge they may justify language changes that are generally applicable but make F# shine when used for ML and analytical workloads. These will be considered on a case-by-base basis, and only when they are generally applicable to F# – shoehorning “machine learning features” is not a goal. As always, we welcome suggestions in our language suggestions repository if you are interested in helping F# evolve with this as inspiration.
Cheers, and happy F# coding!
0 comments