This post is part of our series about F# 9. Check out the full series overview for more posts about shiny new F# features!
Did you hear the good news? Nullable reference types have been improved with the release of F# 9.
What is null anyway?
The special null
value is given to reference types when they have not been assigned a value.
It is purposefully hard to create in standard F#, as the concept of having data which may not exist is handled using the Option
type. To demonstrate, consider the code below. The F# compiler will complain if you try to assign null to an F# type:
type Fruit =
| Orange
| Banana
// This won't compile: The type 'Fruit' does not have 'null' as a proper value
let someFruit: Fruit = null
Non-F# types can be assigned null
from F# code…
// Neither of these cause compile errors
let someString: string = null
let someList: System.Collections.Generic.List<_> = null
Although possible, frequent use of null
assignment with non-F# types is generally not recommended!
Why do we care about null if it is so rare in F#?
Null is quite common in C#. Although C# devs will minimise null propagation (hopefully! 🤞), the fact remains that it exists.
Sometimes there is no suitable F# library for something you need to do; but there might be a C# one, and all of a sudden you find yourself doing C# to F# interop! And that comes with the risk of rogue nulls infiltrating your lovely, previously null-free F# code.
How to handle null in F#
In short, check for it and recover ASAP. This means detecting it for example in an if
or match
statement and taking appropriate action.
In the past F# would force you to box
potentially null values before they could be matched.
// This won't compile: The type 'Fruit' does not have 'null' as a proper value
let isFruitNull (f: Fruit) = f = null
// This will compile as expected
let isFruitNullBoxed (f: Fruit) = box f = null
New syntax for F# 9
The new syntax is somewhat reminicscent of TypeScript's unions, using a pipe (|
) between the types. You can specify that a value could be null in the type signature, by adding | null
(or null |
as preferred).
// New syntax for F# 9, allowing a more streamlined experience
let isFruitNull (f: Fruit | null) = f = null
Of course you might not want to make an isNull
function for every type, but with the new syntax you can also use null
in pattern matching directly:
let getColourNullSafe (f: Fruit | null) =
match f with
| null -> "There is no fruit"
| Orange -> "Orange"
| Banana -> "Yellow"
let todaysLunch : Fruit | null = Orange
let tomorrowsLunch : Fruit | null = null
getColourNullSafe todaysLunch |> printfn "%s" // prints "Orange"
getColourNullSafe tomorrowsLunch |> printfn "%s" // prints "There is no fruit"
Null checking
The F# 9 compiler can check for nulls but this is turned off by default.
To switch it on, add <Nullable>enable</Nullable>
in your .fsproj
or --checknulls+
on the command line. If you want to enable it for F# interactive in conjunction with VS Code you can enable it by adding the following to your settings.json
:
"FSharp.FSIExtraInteractiveParameters": [
"--checknulls+"
],
"FSharp.FSIExtraSharedParameters": [
"--checknulls+"
]
An example of the sort of error message you might see with these checks enabled:
type UserId = | UserId of int
let getUserIdValue (UserId v) = v
let userId: UserId | null = UserId 12345
// Nullness warning: The types 'UserId' and 'UserId | null' do not have equivalent nullability.
printfn $"User ID value is {getUserIdValue userId}"
The warning message doesn't appear at all unless the <Nullable>enabled</Nullable>
flag is set.
Teaching an old dog new tricks
The new F# 9 compiler can provide these benefits even for projects which don't target .NET 9! If this seems confusing, then you should know that SDK version is separate to runtime version - you can use new tools (that's the SDK) and target older runtimes - especially useful if you prefer to target LTS versions, such as .NET 8. Check out this article Matt wrote for a more detailed look at SDK version vs runtime version.
Option, the alternative approach
An alternative to this kind of null checking is to use Option.ofObj
to convert from nullable value to Option
at the boundaries, but it is a matter of personal preference.
Here is the above example extended with another getColour
variation using Option.ofObj
:
let getColour (f: Fruit) =
match f with
| Orange -> "Orange"
| Banana -> "Yellow"
let tryGetColour (f: Fruit) =
f
|> Option.ofObj // <- this converts null to None, and f to Some f
|> Option.map getColour
tryGetColour todaysLunch |> printfn "%A" // prints Some "Orange"
tryGetColour tomorrowsLunch |> printfn "%A" // prints None
To be clear - this alternative way is not new to F# 9.
Summary
The new null features released in F# 9 should make interop between languages simpler, which is a win for everyone!