With regards to nullability, F#'s design has always been to simply disallow nulls within the type system. Instead, you use the option
type to signify potential "absence of a value", with compiler support to prevent you unsafely accessing non-existent data:
type Person = { Name : string; Age : int }
let x : Person = null // compiler error
let y : Person option = None
y.Name // compiler error
match y with
| Some person -> $"Hello, {person.Name}!"
| None -> "I don't know who you are."
For everyday coding, this works remarkably effectively. However, F# does not currently prevent nullable assignment or accessors to any reference types defined outside of F#. This includes all classes within the .NET Framework Class Library (FCL) - including core types such as System.String
:
let p = { Name = null; Age = 21 } // allowed
p.Name.Length // null reference exception!
C# has addressed this problem in more recent versions through the use of flow analysis and type annotations (attributes). These attributes have now been pushed through the entire FCL, and F# is now being enhanced to take advantage of them in this pull request. This pull request is still in draft, but it's interesting to see the progression of it, and how it will start to affect everyday F# code. Let's take a look at it in some more detail:
Firstly, code like this will give warnings (which can - and should - be treated as errors):
let cantBeNull : string = null // FS3261 string does not support null
Instead, you will need to annotate types with the new | null
qualifier to make a "nullable" string, and will not be able to access members on nullable reference types without warnings / errors:
let canBeNull : string | null = null // permitted
let canAlsoBeNull : string | null = "test" // also permitted, analogous to (Some "test")
canAlsoBeNull.Length // Gives FS3261 warning
The compiler will respect all nullness annotations built-into the FCL:
let textInput = System.Console.ReadLine() // string | null
let di = DirectoryInfo "test"
let diName = di.Name // string
let parentDi = di.Parent // string | null
As expected, method and function inputs / outputs will also be respected:
let foo (x:string) = 123
foo textInput // FS3261
You'll be able to use pattern matching to safely "unwrap" a nullable reference value, using either keywords or active patterns:
let pmActivePattern =
match textInput with
| Null -> "a blank string"
| NonNull textInput -> textInput
let pmNull =
match textInput with
| null -> "a blank string"
| textInput -> textInput // automagically unwrapped as a non-nullable ref type
Here's a quick screenshot just to illustrate the above samples working in VS2022 using a preview of the compiler:
There's still clearly more work to be done on this feature, and some questions to resolve such as the future of the Option.ofObj
and toObj
functions, but it's shaping up very nicely. I'm looking forward to this feature "closing out" the last part of "null unsafety" within F#!