Announcing F# 5

Phillip Carter

Phillip

Today, we’re excited to announce the general availability of F# 5. It ships with .NET 5. We’ve been working on F# 5 for over the past year now, and we’re excited to share it with everyone.

You can get F# 5 in the following ways:

If you’re using Visual Studio on Windows, you’ll need to upgrade to the latest 16.8 release.

What F# 5 is all about

From F# 4.1 to F# 5, the chief focus for F# has been bringing up great support for .NET Core (now .NET 5). With F# 5, we’re considering this journey mostly complete. F# 5 marks the start of a new era of F# evolution centered around three main things:

  1. Interactive programming
  2. Making analytical-oriented programming convenient and fun
  3. Great fundamentals and performance for functional programming on .NET

We started F# 5 with roughly these same goals, stating in the first preview that “F# 5 is focused on better interactive and analytical programming”. This remains true, and we also added a few more orthogonal features that everyone can enjoy regardless of how they’re using F#.

Using F# 5

F# 5 is the new default language version for the .NET SDK and Visual Studio. Any new or existing project compiled with either of those toolsets will use F# 5.

You can also use F# 5 in the .NET and Jupyter Notebooks support.

Check out the sample repository that shows off some of what you can do with F# 5. You can play with each of the features there instead of starting from scratch.

FSharp.Core now targets netstandard2.0 only

Starting with F# 5, FSharp.Core 5.0.0 and higher now only targets .NET Standard 2.0. We have dropped out .NET Framework 4.5 target and its dependencies to simplify the package and continue to modernize F#.

If you are using F# on .NET Framework 4.7 or older, you should retarget to .NET Framework 4.7.2 or higher. If you cannot do so, then you should explicitly pin your FSharp.Core reference(s) to an older version, and it is also likely that F# 5 is not the best choice for you right now.

If you were targeting .NET Core or .NET Standard already, nothing is different, since you have already been using the .NET Standard 2.0 build of FSharp.Core this entire time.

Package references in F# scripts

F# 5 brings support for package references in F# scripts with #r "nuget:..." syntax. Here’s how it looks for most packages:

Package references support packages with native dependencies, such as ML.NET.

Package references also support packages with special requirements about referencing dependent .dlls. For example, the FParsec package used to require that users manually ensure that its dependent FParsecCS.dll was referenced first before FParsec.dll was referenced in F# Interactive. This is no longer needed, and you can simply just reference the package like this:

Package references are the basis for acquiring any package when using F# in Jupyter Notebooks or VSCode Notebooks.

Special thanks to Steffen Forkmann for supplying a prototype of this feature several years ago, of which much of the final implementation is still based on. Another special thanks to Gauthier Segay for the initial implementation of an extensibility mechanism for the feature, which can be used to “plug in” other dependency managers in the future.

Support for Jupyter, nteract, and VSCode Notebooks

To coincide with package references, F# 5 is fully supported in Jupyter Notebooks, nteract, and VSCode Notebooks.

Here’s an example of what the VSCode Notebooks support looks like:

F# package reference and code in a VSCode notebook cell

It also supports inline charting:

F# plotting code generating a chart in VSCode notebooks

And allows you to import and export Jupyter Notebooks:

Menu showing import and export for jupyter notebooks

VSCode Notebooks themselves are still in preview, but they already support quite a few features:

  • Preliminary language service support
  • Inline charting and formatting of data
  • Compact data format (.dib) that makes code review easy
  • Ability to import Jupyter notebooks (.ipynb) and convert to a .dib
  • Ability to export a .dib notebook as a Jupyter notebook (.ipynb)
  • Sharing F#-defined values with JavaScript cells
  • Sharing F#-defined values with C# cells

There are more features on the roadmap:

  • Better IntelliSense
  • Sharing more complex data between languages

We’d love to have you try it out and give us feedback on what you feel needs to be there. To do so, follow the installation instructions and don’t be shy when filing issues on GitHub!

String Interpolation

String Interpolation is one of the most highly-requested language features and the very first feature that we had an initial design for in the F# Language Design repository. The design has undergone a lot of discussion over the years, but finally a breakthrough on how to best handle it was made by Yatao Li, who also supplied an initial implementation.

F# interpolated strings are fairly similar to C# or JavaScript interpolated strings, in that they let you write code in “holes” inside of a string literal. Here’s a basic example:

However, F# interpolated strings also allow for typed interpolations, just like the sprintf function, to enforce that an expression inside of an interpolated context conforms to a particular type. It uses the same format specifiers.

For more advanced usage, you can write multiple expressions inside of an interpolation (and technically almost an entire program). That said, it’s usually better to keep function definitions outside of interpolated strings as much as possible!

Support for nameof

Another highly-requested feature of F# 5 is nameof which resolves the symbol it’s being used for and produces its name in F# source. This is useful in various scenarios, such as logging, and protects your logging against changes in source code.

The last line will throw an exception and “month” will be shown in the error message.

You can take a name of nearly everything in F#:

Three final additions are changes to how operators work: the addition of the nameof<'type-parameter> form for generic type parameters, and the ability to use nameof as a pattern in a pattern match expression.

The nameof<'type-parameter> form aligns with how typeof and typedefof work in F# today.

Open Type declarations

F# 5 also adds support for open type declarations. An open type declaration is like opening a static class in C#, except with some different syntax and some slightly different behavior to fit F# semantics.

With Open Type Declarations, you can open any type to expose static contents inside of it. Additionally, you can open F#-defined unions and records to expose their contents. For example, this can be useful if you have a union defined in a module and want to access its cases, but don’t want to open the entire module.

Enhanced Slicing

Slicing data types is critical when doing analytical work on sets of data. To that end, we enhanced F# slicing in two areas for release, with one still considered preview.

Consistent behavior for built-in data types

Behavior for slicing the built-in FSharp.Core data types (array, list, string, 2D array, 3D array, 4D array) used to not be consistent prior to F# 5. Some edge-case behavior threw an exception and some wouldn’t. In F# 5, all built-in types now return empty slices for slices that are impossible to generate:

Fixed-index slices for 3D and 4D arrays in FSharp.Core

The built-in 3D and 4D array types have always supported slices, but they did not support fixing a particular index (such as the y-dimension in a 3D array). Now they do!

To illustrate this, consider the following 3D array:

z = 0

xy01
001
123

z = 1

xy01
045
167

What if you wanted to extract the slice [| 4; 5 |] from the array? This is now very simple!

This kind of slice used to not be possible prior to F# 5.

F# quotations improvements

We’re finishing up a fundamental improvement to F# Code Quotations, a metaprogramming feature that lets you generate and manipulate an abstract syntax tree that represents the F# code.

Although powerful, F# Code Quotations have had a severe deficiency up until this point: they didn’t carry “trait calls” to sufficiently represent the actual semantics of the code being “quoted” if it relied on type constraints. A common way this could manifest itself was “allowing” code with arithmetic that would merely throw an exception at runtime.

These enhancements are particularly relevant to translating F# code to run on other runtimes like PyTorch or ONNX, a key scenario we’re exploring as a means to attract developers in more “analytical” domains to F# and .NET. We also anticipate numerous smaller issues that F# developers using F# Code Quotations today had to work around to be resolved, with the code they expect to “just work” actually live up to that phrase.

A trivial example of code that now just works is as follows:

This used to throw an exception. It now emits -1 as you would expect it to.

Applicative Computation Expressions

Computation expressions (CEs) are used today to model “contextual computations”, or in more functional programming friendly terminology, monadic computations. However, they are a more flexible construct than just offering syntax for monads (you can encode monoids or even a computation expression that explicitly violates every monad law if you like).

F# 5 introduces applicative CEs, which are a slightly different form of CE than what you’re perhaps used to. Applicative CEs allow for significantly more efficient computations provided that every computation is independent, and their results are merely accumulated at the end. When computations are independent of one another, they are also trivially parallelizable. This benefit comes at a restriction, though: computations that depend on previously-computed values are not allowed.

The follow example shows a basic applicative CE for the Result type.

Work for Applicative CEs was done in collaboration with G-Research, who frequently contribute to the F# ecosystem. Thanks, folks!

If you’re a library author who exposes CEs in their library today, there are some additional considerations you’ll need to be aware of. These will be documented in the Computation Expressions article by release.

For consumers of applicative CEs, things aren’t too different from the CEs that you already use. The previously-mentioned restriction around independent computations is the key concept to understand.

Improved stack traces in F# async and other computation expressions

Thanks to a contribution by Nino Floris, stack traces coming from caught exceptions in computation expressions (such as F# async) now retain more information. Consider the following code that uses the Ply library:

Prior to F# 5, the origin function would not appear in stack traces without a workaround in the Ply library (and any other library where this is a scenario). Now it shows the full trace:

Improved .NET interop

F# 5 features several improvements to .NET interop.

Interfaces can be implemented at different generic instantiations

You can now implement the same interface at different generic instantiations. Lukas Rieger contributed an initial design and implementation of this feature.

Default interface member consumption

F# 5 lets you consume interfaces with default implementations.

Consider an interface defined in C# like this:

You can consume it in F# through any of the standard means of implementing an interface:

This lets you safely take advantage of C# code and .NET components written in modern C# when they expect users to be able to consume a default implementation.

Better interop with nullable value types

Nullable (value) types (called Nullable Types historically) have long been supported by F#, but interacting with them has traditionally been somewhat of a pain since you’d have to construct a Nullable or Nullable<SomeType> wrapper every time you wanted to pass a value. Now the compiler will implicitly convert a value type into a Nullable<ThatValueType> if the target type matches. The following code is now possible:

Improved runtime performance

.NET 5 brings improvements to the performance of processing .tail calls, which F# emits in various scenarios (especially in recursive and async code). The impact of this on the runtime performance of your app can vary depending on the kind of code you’re writing, as not all .tail calls are equal. However, you should generally expect an improvement to runtime performance compared to .NET Core 3.1.

Improved Map and Set performance in FSharp.Core

The runtime performance of the Map and Set data structure has improved considerably thanks to work by Victor Baybekov.

For Map, the improved operations are:

  • Retrieval via index – up to 50% faster
  • containsKey – up to 50% faster
  • count – up to 50% faster
  • iter – up to 15% faster
  • add – up to 40% faster
  • remove – up to 33% faster

For Set, the improved operations are:

  • containsKey – up to 40% faster
  • isSubsetOf – up to 40% faster
  • max – up to 50% faster
  • count – up to 50% faster
  • add – up to 30% faster
  • remove – up to 15% faster

Memory usage for each operation is also slightly improved. Thanks, Viktor!

Improved compiler performance

F# 5 brings along some performance improvements for the compiler and editor tooling.

F# compiler performance has steadily improved over the years. I’ll demostrate this by compiling the core project in the FSharpPlus library. This core project makes use of a lot of F# constructs in a way that acts as a great stress test for the compiler itself. I ran the following commands against F# 5, F# 4.7, and F# 4.5.

dotnet clean
dotnet msbuild /m:1 /clp:PerformanceSummary

These commands clean the output directories and force msbuild to run serially (though it would likely be serial anyways since it’s compiling a single project), and reports timings for all tasks run during build. The following table shows the time it took for the Fsc task to complete, which is the F# compiler:

F# versionTime to compile (second, rounded)
F# 549 seconds
F# 4.768 seconds
F# 4.5101 seconds

There was a big improvement from F# 4.5 to F# 4.7, and another big jump with F# 5 as well!

Your own results may vary a bit depending on a variety of factors, but if you try this out yourself you should see a fairly similar spread. It’s also worth noting that everything else in the .NET toolchain has improved too, so it’s not just the F# compiler getting faster when you use F# 5 with .NET 5.

Improved compiler analysis for library authors

F# 5 also includes a compiler feature that checks XML documentation against the signatures they document if you opt into it. This can be turned on in a project file via the OtherFlags property:

This will warn when the following code is compiled because the name of the parameter in the XML documentation doesn't match the name of the parameter in source.

This is a helpful setting for library authors who want to ensure coherence between their libraries and documentation.

Preview features

F# 5 is also bringing along two new preview features. To use these, you'll need to set your <LangVersion>preview</LangVersion> like so:

Preview: reverse indexes

We decided to keep the ability to do reverse indexes in preview for F# 5. There are still some quirks to work out with respect to System.Range and System.Index interop that we want to get right before releaseing fully.

The syntax is ^idx. Here’s how you can an element 1 value from the end of a list:

You can also define reverse indexes for your own types. To do so, you’ll need to implement the following method:

GetReverseIndex: dimension: int -> offset: int

Here’s an example for the Span<'T> type:

We feel that these three enhancements will make slicing data types more convenient in F# moving forward. What do you think?

Preview: overloads of custom keywords in computation expressions

Computation expressions are a powerful feature for library and framework authors. They allow you to greatly improve the expressiveness of your components by letting you define well-known members and form a DSL for the domain you're working in.

Diego Esmerio and Ryan Riley contributed a design an implementation to allow for overloading custom keywords in computation expressions. This new feature allows code like the following to be written:

Prior to this change, you could write the InputBuilder type as it is, but you couldn't use it the way it's used in the previous example. Since overloads, optional parameters, and now System.ParamArray types are allowed, everything just works as you'd expect it to.

What's next

Now that F# 5 is released, we're moving our focus to a few areas:

  1. Improving our OSS infrastructure
  2. Core F# tooling improvements
  3. Planning for the next F# version

Today, the F# development repository is quite a complex codebase. There are several test suites that were created at various points in history, one of which being particularly challenging to work with because it doesn't load in any IDE tooling. We feel that it's unfair to ask open source contributors to do things like add or update tests when this test suite is concerned, so we're migrating it to be modern. The codebase also builds two versions of the F# compiler, called the "desktop compiler" and the "CoreCLR compiler". From a technical standpoint, the "CoreCLR compiler" can be used to build any F# project targeting any .NET flavor on any OS. The only reason why we still build both is technical debt that we intend on paying off. From an open source contributor's standpoint, this should simplify things as well.

We also plan on making significant F# tooling improvements over time. We've already started this work. One of the first steps was to incorporate FSharp.Compiler.Service into our build and packaging infrastructure so that any consumers of this package (e.g., the Ionide plugin in VSCode) can simply add a NuGet feed and get nightly updates. Next, we'll work towards retiring the FSharp.Compiler.Private project, which is functionally equivalent to FSharp.Compiler.Service, so that all F# tooling consumes the same functionality in the same way. From there, we intend on working with the F# community to eventually harden the F# LSP implementation so that it powers all F# editor tooling (including Visual Studio). The long-term goal is that all F# tools have consistent behavior and feature availability, with individual editors adding their own "flavor" and unique feature set on top of that behavior so that F# programmers can pick the tool they like the most.

Additionally, we're continuing to invest in F# tooling for Visual Studio by improving performance and adding several features. One such feature is called "Inline Hints", which you can activate by pressing and holding a key command. This lets you see the inferred types for all declarations and the names of F# parameters when applied (as if they were named parameters):

F# inline hints

The intention behind a feature like this is to let you effectively ask the compiler what it's chosen to infer for your code at any time. We're interested in incorporating other features that make things more productive for F# developers.

Finally, we're planning the next version of the F# language. There are several things on the docket already:

  1. Finishing reverse indices to account for .NET Range and Index interop
  2. Finishing overloads for custom computation expressions
  3. Adding support for a task { } computation expression with state machine generation
  4. Enhancements to the F# type system to better support efforts like DiffSharp

The list for new F# language features is constantly evolving and new requirements come in and we adjust accordingly. However, we're still committed to the direction that we set out on with F# 5. We want to continue to make F# a delightful language for interactive and analytical programming. You can expect to see more goodness in the near future.

Cheers, and happy F# coding!

9 comments

Leave a comment