Announcing F# 4.6
We’re excited to announce general availability of F# 4.6 and the F# tools for Visual Studio 2019! In this post, I’ll show you how to get started, explain the F# 4.6 feature set, give you an update on the F# tools for Visual Studio, and talk about what we’re doing next.
F# 4.6 was developed entirely via an open RFC (requests for comments) process. The F# community has offered very detailed feedback in discussions for this version of the language. You can view all RFCs that correspond with this release here:
First, install either:
If you are a Visual Studio user, you will also get an appropriate .NET Core installed by default.
The only language change in F# 4.6 is the introduction of Anonymous Record types. Although it is a single feature, it can be used in numerous contexts, which naturally means I’ll enumerate all the (useful) use cases I can think of.
From an F#-only perspective, Anonymous Records are F# record types that don’t have explicit names and can be declared in an ad-hoc fashion. Although they are unlikely to fundamentally change how you write F# code, they do fill many smaller gaps F# programmers have encountered over time, and can be used for succinct data manipulation that was not previously possible.
They’re quite easy to use. For example, here how you can interact with a function that produces an anonymous record:
However, they can be used for more than just basic data containers. The following expands the previous sample to use a more type-safe printing function:
If you call `printCircleStats` with an anonymous record that had the same underlying data types but different labels, it fails to compile:
This is exactly how F# record types work, except everything has been declared ad-hoc rather than up-front. This has benefits and drawbacks depending on your particular situation, so I recommend using anonymous records judiciously rather than replacing all your up-front F# record declarations.
Struct anonymous records
Anonymous records can also be structs by using the struct keyword:
You can call a function that takes a struct anonymous record like this:
Or you can use “structness inference” to elide the `struct` at the call site:
Structness inference will treat the instance of the anonymous record you created and passed in as if it were a struct.
Note that the reverse is not true:
It is not currently possible to define `IsByRefLike` or `IsReadOnly` struct anonymous record types. There is a language suggestion that proposes this enhancement, but due to oddities in syntax it is still under discussion.
Taking things further
Anonymous records are great for adding a bit more readability to ephemeral data, but they can also be used in a broader set of more advanced contexts. I’ll go through some of them here.
Anonymous records are serializable
You can serialize and deserialize anonymous records:
Here’s a sample library that is also called in another project:
This may make things easier for scenarios like lightweight data going over a network in a system made up of microservices.
Anonymous records can be combined with other type definitions
You may have a tree-like data model in your domain, such as the following example:
It is typical to see cases modeled as tuples with named union fields, but as data gets more complicated, you may extract each case with records:
This recursive definition can now be shortened with anonymous records if it suits your codebase:
As with previous examples, this technique should be applied judiciously and when applicable to your scenario.
Anonymous records ease the use of LINQ in F#
F# programmers typically prefer using the List, Array, and Sequence combinators when working with data, but it can sometimes be helpful to use LINQ. This has traditionally been a bit painful, since LINQ makes use of C# anonymous types.
With anonymous records, you can use LINQ methods just as you would with C# and anonymous types:
Anonymous records ease working with Entity Framework and other ORMs
F# programmers using F# query expressions to interact with a database should see some minor quality of life improvements with anonymous records.
For example, you may be used to using tuples to group data with a `select` clause:
But this results in columns with names like `Item1` and `Item2` that are not ideal. Prior to anonymous records, you would need to declare a record type and use that. Now you don’t need to do that:
No need to specify the record type up front! This makes query expressions much more aligned with the actual SQL that they model.
Anonymous records also let you avoid having to create `AnonymousObject` types in more advanced queries just to create an ad-hoc grouping of data for the purposes of the query.
Anonymous records ease the use of custom routing in ASP.NET Core
You may be using ASP.NET Core with F# already, but may have run into an awkwardness when defining custom routes. As with previous examples, this could still be done by defining a record type up front, but this has often been seen as unnecessary by F# developers. Now you can do it inline:
It’s still not ideal due to the fact that F# is strict about return types (unlike C#, where you need not explicitly ignore things that return a value). However, this does let you remove previously-defined record definitions that served no purpose other than to allow you to send data into the ASP.NET middleware pipeline.
Copy and update expressions with anonymous records
As with Record types, you can use copy-and-update syntax with anonymous records:
However, copy-and-update expressions do not restrict the resulting anonymous record to be the same type:
The original expression can also be a record type:
You can also copy data to and from reference and struct anonymous records:
The use of copy-and-update expressions gives anonymous records a high degree of flexibility when working with data in F#.
Equality and pattern matching
Anonymous records are structurally equatable and comparable:
However, the types being compared must have the same “shape”:
Although you can equate and compare anonymous records, you cannot pattern match over them. This is for three reasons:
- A pattern must account for every field of an anonymous record, unlike record types. This is because anonymous records do not support structural subtyping – they are nominal types.
- There is no ability to have additional patterns in a pattern match expression, as each distinct pattern would imply a different anonymous record type.
- The requirement to account for every field in an anonymous record would make a pattern more verbose than the use of “dot” notation.
Instead, “dot”-syntax is used to extract values from an anonymous record. This will always be at most as verbose as if pattern matching were used, and in practice is likely to be less verbose due to not always extracting every value from an anonymous record. Here’s how to work with a previous example where anonymous records are a part of a discriminated union:
There is currently an open suggestion to allow pattern matching on anonymous records in the limited contexts that they could actually be enabled. If you have a proposed use case, please use that issue to discuss it!
It wouldn’t be another F# release without additions to the F# Core Library!
The ValueOption type introduces in F# 4.5 now has a few more goodies attached to the type:
- The DebuggerDisplay attribute to help with debugging
- IsNone, IsSome, None, Some, op_Implicit, and ToString members
This gives it “parity” with the Option type.
Additionally, there is now a ValueOption module containing the same functions the the `Option` module has:
This should alleviate concerns that `ValueOption` is the weird sibling of `Option` that doesn’t get the same set of functionality.
tryExactlyOne for List, Array, and Seq
This fine function was contributed by Grzegorz Dziadkiewicz. Here’s how it works:
F# tools for Visual Studio 2019
In addition to releasing F# 4.6, we’ve done a lot of work in the F# tools for Visual Studio, especially around performance for larger solutions. Before going into some details, I’ll reiterate how you may want think about the F# tools for Visual Studio.
You may have noticed that the last release of Visual Studio 2017 is called “update 15.9” or “15.9”. Visual Studio 2017 started at update 15.0 when it first released. Since then, we delivered 8 official updates to F# tooling, including the F# 4.5 release in the 15.8 update. This update cadence was somewhat unexpected for people who were used to the slow update cadence of past Visual Studio versions.
Visual Studio 2019 will continue the frequent, incremental updates for the F# tools. You might notice is that VS 2019 is also called “version 16.0” or “16.0”, especially if you follow F# development on GitHub. Although Visual Studio 2019 is a new version of Visual Studio, the F# tools are an incremental improvement over update 15.9 with a focus on performance.
With this in mind, you can think of the Visual Studio 2019 release and future updates as a continuous evolution of F# tooling. We’re incredibly excited to continue shipping frequent updates to F# and F# tools.
Our primary area of focus for this release has been performance for medium-to-large sized solutions. Historically, the F# compiler and tools have struggled with larger solutions, leading to a lot of memory and CPU usage, and sometimes forcing people to work around the tooling behavior rather than apply their preferred approach to managing source code.
To address this, we started our work by analyzing memory usage, and identified multiple patterns where the F# compiler was rather gluttonous with Large Object Heap (LOH) allocations. One such case we found was a workaround to a bug in 2008 where the IDE could not provide IntelliSense on the very last line of a source file. That workaround resulted in horrible performance characteristics by forcing the re-allocation of the string representing an active source file for every operation in the IDE! To address the performance problem, we removed all allocations of this nature and changed the F# parser so that it could provide enough context to the IntelliSense engine such that IntelliSense at the bottom of a source file would work properly.
Other work included significant reductions in cache sizes, significant reductions in allocations when processing format strings, removing ambient processing of identifiers for suggestions when encountering a compile error, removing LOH allocations for F# symbols when they are finished being typechecked, and removing some unnecessary boxing of value types that are used in lots of IDE features. This performance work was done in collaboration with some of our wonderful OSS contributors: Avi Avni, Chet Husk, Steffen Forkmann, and Eugene Auduchinok. We’re extremely grateful to have such wonderful people who are excited to help improve the F# experience!
F# users with medium to large solutions (50+ projects) should notice things running smoother, especially over long stretches of work. There’s still a lot to go, especially for solutions using Type Providers extensively. We’re already focusing on addressing some issues there, so you can expect future updates of the F# tools to perform even better over time.
It wouldn’t be a new release without some new features!
Saul Rennison contributed a feature that intelligently indents pasted code based on where your cursor is. If you turn on Smart Indent via Tools > Options > Text Editor > F# > Tabs > Smart this will be on automatically.
We’ve also made a few small changes to IntelliSense to help clean up the experience a bit:
- Backspace after an identifier has been fully typed out will no longer suggest seemingly unrelated items
- The primary constructor after an inherit clause now shows in completion, contributed by Eugene Auduchinok
- Symbols from unopened namespaces will not be shown by default anymore (you can turn it back on in the F# editor settings)
And as you would expect, Anonymous Records have their labels show up in all F# tooling (Go to Definition, Rename, Find All References, etc.)
What we’re working on next
As previously mentioned, we’re continuing our foray into solving deep-rooted performance issues in F# tooling. Although we cannot guarantee that every update in F# tooling comes with performance improvements, it is an ambient priority that we will continually improve throughout the Visual Studio 2019 update timeline.
We also have some big milestones coming up this year:
- F# 5.0 and .NET Core 3.0
- F# Interactive on .NET Core
- F# for Machine Learning
Work on F# 5.0 is already underway, with multiple new features under active development and others planned. Our goals are to align F# 5.0 with .NET Core 3.0, but this will ultimately be a quality call. If certain features are important for you to be in F# 5.0, we encourage you to engage with us either on the F# Language Suggestions or F# Design repositories.
F# Interactive (FSI) is also a top priority for us to align with .NET Core 3.0. You can already try out FSI on .NET Core 3.0 Preview today, and you can expect further improvements to ship in subsequent previews. Additionally, we’re looking to finish up work to allow you to reference packages directly in F# scripts on both .NET Core and .NET Framework. This should fundamentally change how F# scripting is done and greatly simplify existing workflows people have today.
Finally, we’re also devoting significant time in developing a compelling offering for using F# to do machine learning. In addition to being supported on ML.NET, we’re working towards a world-class experience when using F# and Tensorflow. Tensorflow shape checking and shape inference tie quite nicely into the F# type system and tools, which we feel is a differentiator when compared to using Python, Swift, or Scala. This is still an active research area, but over time we expect the experience to become quite solid as F# Interactive experiences on .NET Core also shape up.
Although the total list of new features in F# 4.6 isn’t enormous, there’s a lot to this release! Additionally, our focus on performance should result in a cleaner experience when using F# in Visual Studio 2019. As we continue to make strides in this area with future updates, you can expect things to get better and better.
As always, thank you to the F# community for their contributions, both in code and design discussion, that help us continue to advance the F# language and tools.
Cheers, and happy hacking!