The introduction of nullable reference types in C# represents the biggest change to how .NET developers write code since async/await. Once it goes live, countless libraries will have to be updated with nullable annotations in order for this feature to work correctly. And to ensure interoperability, F# will need to respond in kind.

Current State

F# currently supports several versions of nullability. First there are normal .NET reference types. Today there is no way to unequivocally inform the compiler if a specific reference type variable is nullable or not, so their use in F# is discouraged.

The preferred alternative is Option<T> . Also known as a “maybe” type, this is a type-safe way to express the concept of nullability. When used with idiomatic F# code, you can only read the value after checking to see if it is non-null (not “none” in F# parlance). This is usually done via pattern matching. For example,

match ParseDateTime inputString with | Some(date) -> printfn "%s" (date.ToLocalTime().ToString()) | None -> printfn "Failed to parse the input."

Option<T> is a reference type with Option<T>.None being defined as null . This can lead to a lot of unnecessary memory pressure, so a struct-based alternative called ValueOption<T> was created in F# 4.5. However, ValueOption<T> is missing some features that won’t be complete until F# 4.6 is released.

Another type of null F# developers may need to deal with is Nullable<T> . This is similar to ValueOption<T> , but is restricted to only value types. You see it in the current version of C# and VB when value types are declared with the ? symbol appended to the type.

Classes may also be marked as nullable. If the type has the AllowNullLiteral attribute, then all variables of that type are treated as nullable. This can be problematic when you want some variables of that type to be nullable but not others.

Design Problems

A fundamental design problem with F# as it currently stands is all of these different forms of nullability are incompatible. Not only are conversions needed between the different types of nulls, there are important differences in how they work. For example, an Option<T> is recursive, allowing you to have an Option<Option<Int32>> while Nullable<T> is not. This can lead to unexpected problems when they are mixed.

When used with traditional .NET reference types, Option<T> introduces a hole into the type system. While you wouldn’t normally see it in F# code, a C# or VB function can create an Option<T> that is not null/none, but contains a null. For example, Option<string>.Some(null) . This can also occur accidentally when wrapping the result of a non-F# library call into an Option<T> . Since the F# compiler doesn’t anticipate this problem, you can still get a null reference exception after verifying that an Option<T> doesn’t contain a none.

Another way this incompatibility comes into play is the CLIMutable attribute. Normally record types are immutable, but that makes them incompatible with ORMs. This attribute fixes that problem, but introduces a new one. Now that the record is mutable, nulls can slip in after the object is created, breaking the assumption that records don’t contain nulls.

Nullable Reference Types Inspired by C#

Just like C#, backwards compatibility is needed for F#. Any program that compiles under F# 4.x must compile under F# 5 as well. But at the same time, there is a strong desire to take advantage of C#’s annotations that indicate whether or not a value can be null.

The current plan is to indicate nullable variables with a ? suffix just like we see in C#. And like C# 8, you will get warnings if you try to invoke a method or property on a nullable variable without first checking it for null. Likewise, assigning a null to a non-nullable variable is only a warning so legacy code continues to compile.

This functionality is considered to be opt-in. New projects will have it turned on by default, while existing projects will have it turned off by default.

The examples below were provided by the Nullable Reference Types proposal and are subject to change.

// Declared type at let-binding let notAValue : string? = null // Declared type at let-binding let isAValue : string? = "hello world" let isNotAValue2 : string = null // gives a nullability warning let getLength (x: string?) = x.Length // gives a nullability warning since x is a nullable string // Parameter to a function let len (str: string?) = match str with | null -> -1 | NonNull s -> s.Length // binds a non-null result // Parameter to a function let len (str: string?) = let s = nullArgCheck "str" str // Returns a non-null string s.Length // binds a non-null result // Declared type at let-binding let maybeAValue : string? = hopefullyGetAString() // Array type signature let f (arr: string?[]) = () // Generic code, note 'T must be constrained to be a reference type let findOrNull (index: int) (list: 'T list) : 'T? when 'T : not struct = match List.tryItem index list with | Some item -> item | None -> null

As you can see, the new syntax fits well with existing F# patterns, even supporting pattern matching in a similar way to Option<T> . It is noted in the proposal this may make it too easy to use and “there is concern that they may thus become mainstream in F# usage.”

One programmer responded, “Why wouldn’t this become mainstream? It´s easier to write, offers better performance, and requires less memory.”

There are also a set of helper functions to add in general code and pattern matching.

isNull : Determines whether the given value is null.

: Determines whether the given value is null. nonNull : Asserts the value is non-null. Raises a NullReferenceException when value is null, otherwise returns the value. (This acts like ! in C#.)

: Asserts the value is non-null. Raises a when value is null, otherwise returns the value. (This acts like ! in C#.) withNull : Converts the value to a type that admits null as a normal value.

: Converts the value to a type that admits null as a normal value. (|NonNull|) : When used in a pattern asserts the value being matched is not null.

: When used in a pattern asserts the value being matched is not null. (|Null|NotNull|) : An active pattern which determines whether the given value is null.

The full function signatures are available in the proposal.

Nullable<T> Support

Support for Nullable<T> is also desirable, but that leads to some questions about syntax and consistency. While a nullable reference type is a normal variable with an attribute, a nullable value type is its own type that conditionally wraps a value.

The consequence is you cannot have a generic method where the type parameter is nullable unless it is either constrained to be a struct or constrained to not be a struct. (We saw this same problem in the article titled Adapting Projects to Use C# 8 and Nullable Reference Types.)

Since the list of helper functions above are all generic, value versions such as isNullV are offered by the prototype.

Type Inference

Type inference is one of the major ways F# differs from most other .NET languages. While VB and C# have a limited type inference for local variable, F# has the ability to infer types across wide parts of the application including public APIs.

In order to enable nullable reference types with type inference, new rules are needed.

Nullable annotations on reference types are ignored when deciding type equivalence, though warnings are emitted for mismatches.

Nullable annotations on reference types are ignored when deciding type subsumption, though warnings are emitted for mismatches. (TBD: give the exact specification in terms or expected and actual types)

Nullable annotations on reference types are ignored when deciding method overload resolution, though warnings are emitted for mismatches in argument and return types once an overload is committed.

Nullable annotations on reference types are ignored for abstract slot inference, though warnings are emitted for mismatches.

Nullable annotations on reference types are ignored when checking for duplicate methods.

The common theme across all of these is a nullable reference type is literally the same type as its non-nullable alternative, which is why improper conversions between the two are merely warnings, not compiler errors.

In the case of arrays, lists, and sequences, nullability is inferred from the first element. If this isn’t desired, an explicit type annotation can be added.

Additional Considerations

The type obj is always considered to be nullable so the type obj? is not valid.

Non-nullable reference types such as string are no longer considered to have a default value and cannot be used with the DefaultValue attribute.