Skip to content

Add nullable reference types [RFC FS-1060] #577

@0x53A

Description

@0x53A

RFC: https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1060-nullable-reference-types.md

[Updated description by @dsyme]

C# 8.0 is likely to ship with a feature where new C# projects opt-in by default to making reference types "without-null" by default), with explicit annotations for reference types "with-null". The corresponding metadata annotations produced by this feature are likely to start to be used by .NET Framework libraries.

F#-defined types are "without-null" by default, however .NET-defined types like string and array and any other types defined in .NET libraries are "with-null" by default. The suggestion is to make new F# 5.0 projects opt-in to having .NET reference types be "without-null" by default when mentioned in F# code, and to interoperate with .NET metadata produced by C# 8.0 indicating when types are "with-null".

Terminology and working assumptions

These terms are used interchangeably:

  • "null as a abnormal value", "string without null", "non-nullable string"

Likewise

  • "null as a normal value", "string with null", "nullable string"

For the purposes of discussion we will use string | null as the syntax for types explicitly annotated to be "with null". You will also see string? in some samples

We will assume this feature is for F# 5.0 and is activated by a "/langlevel:5.0" switch that is on by default for new projects.

Proposed Design Principles

We are at the stage of trying to clarify the set of design principles to guide this feature:

  1. We should aim that F# should remain "almost" as simple to use as it is today. Indeed, the aim should be that the experience of using F# as a whole is simpler, because the possibility of nulls flowing in from .NET code for types like string is reduced and better tracked.

  2. The value for F# here is primarily in flowing non-nullable annotations into F# code from .NET libraries, and vice-versa, and in allowing the F# programmer to be explicit about the non-nullability of .NET types.

  3. Adding with-null/without-null annotations should not be part of routine F# programming

  4. There is a known risk of "extra information overload" in some tooling, e.g. tooltips. Nullability annotations/information may need to be suppressed and simplified in some types shown in output in routine F# programming. There is discussion about how this would be tuned in practice

  5. F# users should primarily only experience/see this feature when interoperating with .NET libraries (the latter should be rarely needed)

  6. The feature should produce warnings only, not hard errors

  7. The feature is backwards compatible, but only in the sense all existing F# projects compile without warning by default. Placing F# 4.x code into F# 5.0 projects may give warnings.

  8. F# non-nullness is reasonably "strong and trustworthy" today for F#-defined types and routine F# coding. This includes the explicit use of option types to represent the absence of information. The feature should not lead to a weakening of this trust nor a change in F# methodology that leads to lower levels of safety.

Notes from original posting

Note that there are a few related proposals (see below), but I couldn't find an exact match in this repo.

One reason I am adding this proposal is that I have lately been working in a mixed C# / F# solution, where I DO need to work with null more often than I like.

The other reason is that we should be aware of the parallel proposal for C# (dotnet/csharplang#36).

My main question is: Is there a minimal implementation for F#, that eases working with C# types NOW, without blocking adoption of future C# evolutions?

The existing way of approaching this problem in F# is ...

Types declared in F# are non-nullable by default. You can either make them nullable with AllowNullLiteralAttribute, or wrap them in an Option<'T>.

Types declared either in C#, or in a third-party F# source with AllowNullLiteralAttribute are always nullable, so in theory you would need to deal with null for every instance. In practice, this is often ignored and may or (often even worse) may not fail with a null-reference exception.

I propose we ...

I propose we add a type modifier to declare that this instance shall never be null.
This will be most useful in a function parameter.

Because I do not want to discuss the actual form of that modifier (attribute, keyword, etc), I will use 'T foo as meaning non-nullable 'T, similar to 'T option.

Example:

let stringLength (s:string foo) = s.Length

Calling this method with a vanilla string instance would produce a hard error.

How can a nullable type be converted to a non-nullable?

There should be at least a limited form of flow analysis.

Two examples would be if-branches and pattern matching:

let s = ... // vanilla (nullable) string

(* 1 - pattern matching *)

match s with
| null -> 0
| s -> stringLength s

// Note that this does not change the original variable s - the new s shadows the old s. I can also use a different variable name:

match s with
| null -> 0
| s2 ->
    // this fails:
    // stringLength s
    // this works:
    stringLength s2



(* 2 - explicit null-checking *)

if s = null then
    ()
else
    // now the compiler knows s is non-nullable
    stringLength s

There probably also needs to be a shorthand to force a cast, similar to Option.Value, which will throw at runtime if the option is null.

Runtime behavior

The types are erased to attributes.
You can't overload between nullable and non-nullable (may in combination with inline?)
When a non-null type modification is used in a function parameter, the compiler should insert a null-check at the beginning of the method.
The compiler should also add a null-check for all hard casts.

Pros and Cons

The advantages of making this adjustment to F# are ...

stronger typing.

The disadvantages of making this adjustment to F# are ...

yet another erased type modifier, like UOM, aliases.

Extra information

Estimated cost (XS, S, M, L, XL, XXL): L

Related suggestions: (put links to related suggestions here)

#552: Flow based null check analysis for [<AllowNullLiteralAttribute>] types and alike

Affidavit (must be submitted)

Please tick this by placing a cross in the box:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I would be willing to help implement and/or test this
  • I or my company would be willing to help crowdfund F# Software Foundation members to work on this

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions