From ded69592e44267bca6333eb9cc5a61e21c5e0817 Mon Sep 17 00:00:00 2001 From: Petr Date: Fri, 20 Dec 2024 12:26:26 +0100 Subject: [PATCH 01/15] Nullable reference types in F# --- .../generics/constraints.md | 5 ++ .../language-reference/pattern-matching.md | 8 +++ .../language-reference/values/null-values.md | 66 ++++++++++++++++++- .../component-design-guidelines.md | 55 +++++++++++++++- docs/fsharp/style-guide/conventions.md | 30 +++++++++ 5 files changed, 162 insertions(+), 2 deletions(-) diff --git a/docs/fsharp/language-reference/generics/constraints.md b/docs/fsharp/language-reference/generics/constraints.md index 474fc7bdbe34d..e4bed4b04c6e7 100644 --- a/docs/fsharp/language-reference/generics/constraints.md +++ b/docs/fsharp/language-reference/generics/constraints.md @@ -21,6 +21,7 @@ There are several different constraints you can apply to limit the types that ca |----------|------|-----------| |Type Constraint|*type-parameter* :> *type*|The provided type must be equal to or derived from the type specified, or, if the type is an interface, the provided type must implement the interface.| |Null Constraint|*type-parameter* : null|The provided type must support the null literal. This includes all .NET object types but not F# list, tuple, function, class, record, or union types.| +|Not Null Constraint|*type-parameter* : not null|The provided type must not support the null literal. This includes both `null` annotated types and types which have null as their representation value (such as the option type).| |Explicit Member Constraint|[(]*type-parameter* [or ... or *type-parameter*)] : (*member-signature*)|At least one of the type arguments provided must have a member that has the specified signature; not intended for common use. Members must be either explicitly defined on the type or part of an implicit type extension to be valid targets for an Explicit Member Constraint.| |Constructor Constraint|*type-parameter* : ( new : unit -> 'a )|The provided type must have a parameterless constructor.| |Value Type Constraint|*type-parameter* : struct|The provided type must be a .NET value type.| @@ -54,6 +55,10 @@ type Class2<'T when 'T :> System.IComparable> = type Class3<'T when 'T : null> = class end +// Not Null constraint +type Class3<'T when 'T : not null> = + class end + // Member constraint with instance member type Class5<'T when 'T : (member Method1 : 'T -> int)> = class end diff --git a/docs/fsharp/language-reference/pattern-matching.md b/docs/fsharp/language-reference/pattern-matching.md index 5535213a35d39..545a3274852ee 100644 --- a/docs/fsharp/language-reference/pattern-matching.md +++ b/docs/fsharp/language-reference/pattern-matching.md @@ -210,6 +210,14 @@ The following example uses the null pattern and the variable pattern. [!code-fsharp[Main](~/samples/snippets/fsharp/lang-ref-2/snippet4817.fs)] +Null pattern is also recommended for the F# 9 [nullability capabilities](./values/null-values.md#null-values-starting-with-f-9). +```fsharp +let len (str: string | null) = + match str with + | null -> -1 + | s -> s.Length +``` + ## Nameof pattern The `nameof` pattern matches against a string when its value is equal to the expression that follows the `nameof` keyword. for example: diff --git a/docs/fsharp/language-reference/values/null-values.md b/docs/fsharp/language-reference/values/null-values.md index f5187e754e22e..9f7e5b7d109f3 100644 --- a/docs/fsharp/language-reference/values/null-values.md +++ b/docs/fsharp/language-reference/values/null-values.md @@ -7,7 +7,7 @@ ms.date: 08/15/2020 This topic describes how the null value is used in F#. -## Null Value +## Null Values Prior To F# 9 The null value is not normally used in F# for values or variables. However, null appears as an abnormal value in certain situations. If a type is defined in F#, null is not permitted as a regular value unless the [AllowNullLiteral](https://fsharp.github.io/fsharp-core-docs/reference/fsharp-core-allownullliteralattribute.html#Value) attribute is applied to the type. If a type is defined in some other .NET language, null is a possible value, and when you are interoperating with such types, your F# code might encounter null values. @@ -31,6 +31,70 @@ You can use the following code to check if an arbitrary value is null. [!code-fsharp[Main](~/samples/snippets/fsharp/lang-ref-1/snippet703.fs)] +## Null Values starting with F# 9 + +In F# 9, extra capabilities are added to the language to deal with reference types which can have `null` as a value. Those are off by default - to turn them on, the following property must be put in your project file: + +```xml +enable +``` + +To explicitly opt-in into nullability, a type declaration has to be suffixed with the new syntax: + +```fsharp +type | null +``` + +The bar symbol `|` has the meaning of a logical OR in the syntax, building a union of two disjoint sets of types: the underlying type, and the nullable reference. This is the same syntactical symbol which is used for declaring multiple cases of an F# discriminated union: `type AB = A | B` carries the meaning of either `A`, or `B`. + +The nullable annotation ` | null` can be used at all places where a reference type would be normally used: + +- Fields of union types, record types and custom types. +- Type aliases to existing types. +- Type applications of a generic type. +- Explicit type annotations to let bindings, parameters or return types. +- Type annotations to object-programming constructs like members, properties or fields. + +```fsharp +type AB = A | B +type AbNull = AB | null + +type RecordField = { X: string | null } +type TupleField = string * string | null + +type NestedGenerics = { Z : List | null> | null } +``` + +The bar symbol `|` does have other usages in F# which might lead to syntactical ambiguities. In such cases, parentheses are needed around the null-annotated type: + +```fsharp +// Unexpected symbol '|' (directly before 'null') in member definition +type DUField = N of string | null +``` + +Wrapping the same type into a pair of `( )` parentheses fixes the issue: + +```fsharp +type DUField = N of (string | null) +``` + +When used in pattern matching, `|` is used to separate different pattern matching clauses. + +```fsharp +match x with +| ?: string | null -> ... +``` + +This snippet is actually equivalent to code first doing a type test against the `string` type, and then having a separate clause for handling null: + +```fsharp +match x with +| ?: string +| null -> ... +``` + +Note that all these capabilities were added to the language for the interoperability purposes. Using `| null` within F# code is not considered idiomatic for denoting missing information - for that purpose use options, as described above. + ## See also - [Values](index.md) diff --git a/docs/fsharp/style-guide/component-design-guidelines.md b/docs/fsharp/style-guide/component-design-guidelines.md index f8ab19b497e16..88cb3232f801f 100644 --- a/docs/fsharp/style-guide/component-design-guidelines.md +++ b/docs/fsharp/style-guide/component-design-guidelines.md @@ -693,11 +693,64 @@ let checkNonNull argName (arg: obj) = | null -> nullArg argName | _ -> () -let checkNonNull` argName (arg: obj) = +let checkNonNull' argName (arg: obj) = if isNull arg then nullArg argName else () ``` +Starting with F# 9, you can leverage the new ` | null` [syntax](../language-reference/values/null-values.md#null-values-starting-with-f-9) to make the compiler indicate possible null values and where they need handling. The above code will then start producing warnings: + +```fsharp +let checkNonNull argName (arg: obj) = + match arg with + // nullness warning: the type 'obj' does not support 'null' + | null -> nullArg argName + | _ -> () + +let checkNonNull' argName (arg: obj) = + // nullness warning: the type 'obj' does not support 'null' + if isNull arg then nullArg argName + else () +``` + +To address the warnings, you need to explicitly specify `null` as a possible argument value: + +```fsharp +let checkNonNull argName (arg: obj | null) = + match arg with + | null -> nullArg argName + | _ -> () + +let checkNonNull' argName (arg: obj | null) = + if isNull arg then nullArg argName + else () +``` + +On the other hand, you'll get a warning if the compiler detects that a possible null value is not handled: + +```fsharp +let printLineLength (s: string) = + printfn "%i" s.Length + +let readLineFromStream (sr: System.IO.StreamReader) = + let line = sr.ReadLine() + // nullness warning: The types 'string' and 'string | null' + // do not have equivalent nullability + printLineLength line +``` + +These warnings should be addressed using idiomatic F# [null pattern](../language-reference/pattern-matching.md#null-pattern) via pattern matching: +```fsharp +let printLineLength (s: string) = + printfn "%i" s.Length + +let readLineFromStream (sr: System.IO.StreamReader) = + let line = sr.ReadLine() + match line with + | null -> () + | s -> printLineLength s +``` + #### Avoid using tuples as return values Instead, prefer returning a named type holding the aggregate data, or using out parameters to return multiple values. Although tuples and struct tuples exist in .NET (including C# language support for struct tuples), they will most often not provide the ideal and expected API for .NET developers. diff --git a/docs/fsharp/style-guide/conventions.md b/docs/fsharp/style-guide/conventions.md index 61658bebff99a..aedca2bc62e5b 100644 --- a/docs/fsharp/style-guide/conventions.md +++ b/docs/fsharp/style-guide/conventions.md @@ -687,6 +687,36 @@ module Array = For legacy reasons some string functions in FSharp.Core still treat nulls as empty strings and do not fail on null arguments. However do not take this as guidance, and do not adopt coding patterns that attribute any semantic meaning to "null". +### Leverage F# 9 null syntax at the API boundaries + +F# 9 adds [syntax](../language-reference/values/null-values.md#null-values-starting-with-f-9) to explicitly state that a value can be null. It's designed to be used on the API boundaries, to make the compiler indicate the places where null handling null is missing. + +Here is an example of the valid usage of the syntax: +```fsharp +let processStream (stream: System.IO.StreamReader) = + let processLine (line: string | null) = + match line with + | null -> (); false + | s -> printfn "%s" s; true + + while processLine(stream.ReadLine()) do () + stream.Close() +``` + +**Avoid** propagating nulls further down your F# code: +```fsharp +let getLineFromStream (stream: System.IO.StreamReader) : string | null = + stream.ReadLine() +``` + +Instead, use idiomatic F# means (e.g., options): +```fsharp +let getLineFromStream (stream: System.IO.StreamReader) : string option = + match stream.ReadLine() with + | null -> None + | s -> Some s +``` + ## Object programming F# has full support for objects and object-oriented (OO) concepts. Although many OO concepts are powerful and useful, not all of them are ideal to use. The following lists offer guidance on categories of OO features at a high level. From dcacb249a45c8fcfd2d68ecf480289061f28e254 Mon Sep 17 00:00:00 2001 From: Petr Date: Fri, 20 Dec 2024 18:00:43 +0100 Subject: [PATCH 02/15] up --- docs/fsharp/language-reference/generics/constraints.md | 2 +- docs/fsharp/language-reference/values/null-values.md | 2 +- docs/fsharp/style-guide/conventions.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/fsharp/language-reference/generics/constraints.md b/docs/fsharp/language-reference/generics/constraints.md index e4bed4b04c6e7..a0e222038400e 100644 --- a/docs/fsharp/language-reference/generics/constraints.md +++ b/docs/fsharp/language-reference/generics/constraints.md @@ -21,7 +21,7 @@ There are several different constraints you can apply to limit the types that ca |----------|------|-----------| |Type Constraint|*type-parameter* :> *type*|The provided type must be equal to or derived from the type specified, or, if the type is an interface, the provided type must implement the interface.| |Null Constraint|*type-parameter* : null|The provided type must support the null literal. This includes all .NET object types but not F# list, tuple, function, class, record, or union types.| -|Not Null Constraint|*type-parameter* : not null|The provided type must not support the null literal. This includes both `null` annotated types and types which have null as their representation value (such as the option type).| +|Not Null Constraint|*type-parameter* : not null|The provided type must not support the null literal. This disallows both `null` annotated types and types which have null as their representation value (such as the option type). This does allow value types, since those can never be null.| |Explicit Member Constraint|[(]*type-parameter* [or ... or *type-parameter*)] : (*member-signature*)|At least one of the type arguments provided must have a member that has the specified signature; not intended for common use. Members must be either explicitly defined on the type or part of an implicit type extension to be valid targets for an Explicit Member Constraint.| |Constructor Constraint|*type-parameter* : ( new : unit -> 'a )|The provided type must have a parameterless constructor.| |Value Type Constraint|*type-parameter* : struct|The provided type must be a .NET value type.| diff --git a/docs/fsharp/language-reference/values/null-values.md b/docs/fsharp/language-reference/values/null-values.md index 9f7e5b7d109f3..25c1134e96af1 100644 --- a/docs/fsharp/language-reference/values/null-values.md +++ b/docs/fsharp/language-reference/values/null-values.md @@ -93,7 +93,7 @@ match x with | null -> ... ``` -Note that all these capabilities were added to the language for the interoperability purposes. Using `| null` within F# code is not considered idiomatic for denoting missing information - for that purpose use options, as described above. +Note that the extra null related capabilities were added to the language for the interoperability purposes. Using `| null` within F# code is not considered idiomatic for denoting missing information - for that purpose, use options (as described above). Read more about null-related [conventions](../../style-guide/conventions.md#nulls-and-default-values) in the style guide. ## See also diff --git a/docs/fsharp/style-guide/conventions.md b/docs/fsharp/style-guide/conventions.md index aedca2bc62e5b..a9c0730882413 100644 --- a/docs/fsharp/style-guide/conventions.md +++ b/docs/fsharp/style-guide/conventions.md @@ -709,7 +709,7 @@ let getLineFromStream (stream: System.IO.StreamReader) : string | null = stream.ReadLine() ``` -Instead, use idiomatic F# means (e.g., options): +**Instead**, use idiomatic F# means (e.g., options): ```fsharp let getLineFromStream (stream: System.IO.StreamReader) : string option = match stream.ReadLine() with From 4c4a7fbe69eb1940af959730e9a23bff619e92a7 Mon Sep 17 00:00:00 2001 From: Petr Date: Fri, 20 Dec 2024 18:06:33 +0100 Subject: [PATCH 03/15] up --- docs/fsharp/language-reference/pattern-matching.md | 3 ++- docs/fsharp/language-reference/values/null-values.md | 2 +- docs/fsharp/style-guide/component-design-guidelines.md | 3 ++- docs/fsharp/style-guide/conventions.md | 3 +++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/fsharp/language-reference/pattern-matching.md b/docs/fsharp/language-reference/pattern-matching.md index 545a3274852ee..d2a25a0334878 100644 --- a/docs/fsharp/language-reference/pattern-matching.md +++ b/docs/fsharp/language-reference/pattern-matching.md @@ -210,7 +210,8 @@ The following example uses the null pattern and the variable pattern. [!code-fsharp[Main](~/samples/snippets/fsharp/lang-ref-2/snippet4817.fs)] -Null pattern is also recommended for the F# 9 [nullability capabilities](./values/null-values.md#null-values-starting-with-f-9). +Null pattern is also recommended for the F# 9 [nullability capabilities](./values/null-values.md#null-values-starting-with-f-9): + ```fsharp let len (str: string | null) = match str with diff --git a/docs/fsharp/language-reference/values/null-values.md b/docs/fsharp/language-reference/values/null-values.md index 25c1134e96af1..82f91b1d9760a 100644 --- a/docs/fsharp/language-reference/values/null-values.md +++ b/docs/fsharp/language-reference/values/null-values.md @@ -47,7 +47,7 @@ type | null The bar symbol `|` has the meaning of a logical OR in the syntax, building a union of two disjoint sets of types: the underlying type, and the nullable reference. This is the same syntactical symbol which is used for declaring multiple cases of an F# discriminated union: `type AB = A | B` carries the meaning of either `A`, or `B`. -The nullable annotation ` | null` can be used at all places where a reference type would be normally used: +The nullable annotation `| null` can be used at all places where a reference type would be normally used: - Fields of union types, record types and custom types. - Type aliases to existing types. diff --git a/docs/fsharp/style-guide/component-design-guidelines.md b/docs/fsharp/style-guide/component-design-guidelines.md index 88cb3232f801f..491681d0bfaac 100644 --- a/docs/fsharp/style-guide/component-design-guidelines.md +++ b/docs/fsharp/style-guide/component-design-guidelines.md @@ -698,7 +698,7 @@ let checkNonNull' argName (arg: obj) = else () ``` -Starting with F# 9, you can leverage the new ` | null` [syntax](../language-reference/values/null-values.md#null-values-starting-with-f-9) to make the compiler indicate possible null values and where they need handling. The above code will then start producing warnings: +Starting with F# 9, you can leverage the new `| null` [syntax](../language-reference/values/null-values.md#null-values-starting-with-f-9) to make the compiler indicate possible null values and where they need handling. The above code will then start producing warnings: ```fsharp let checkNonNull argName (arg: obj) = @@ -740,6 +740,7 @@ let readLineFromStream (sr: System.IO.StreamReader) = ``` These warnings should be addressed using idiomatic F# [null pattern](../language-reference/pattern-matching.md#null-pattern) via pattern matching: + ```fsharp let printLineLength (s: string) = printfn "%i" s.Length diff --git a/docs/fsharp/style-guide/conventions.md b/docs/fsharp/style-guide/conventions.md index a9c0730882413..3498b4cb6b046 100644 --- a/docs/fsharp/style-guide/conventions.md +++ b/docs/fsharp/style-guide/conventions.md @@ -692,6 +692,7 @@ For legacy reasons some string functions in FSharp.Core still treat nulls as emp F# 9 adds [syntax](../language-reference/values/null-values.md#null-values-starting-with-f-9) to explicitly state that a value can be null. It's designed to be used on the API boundaries, to make the compiler indicate the places where null handling null is missing. Here is an example of the valid usage of the syntax: + ```fsharp let processStream (stream: System.IO.StreamReader) = let processLine (line: string | null) = @@ -704,12 +705,14 @@ let processStream (stream: System.IO.StreamReader) = ``` **Avoid** propagating nulls further down your F# code: + ```fsharp let getLineFromStream (stream: System.IO.StreamReader) : string | null = stream.ReadLine() ``` **Instead**, use idiomatic F# means (e.g., options): + ```fsharp let getLineFromStream (stream: System.IO.StreamReader) : string option = match stream.ReadLine() with From 211b3831c2be91a049977de6eda50476519f62e1 Mon Sep 17 00:00:00 2001 From: Petr Date: Tue, 7 Jan 2025 15:46:40 +0100 Subject: [PATCH 04/15] updates --- .../language-reference/active-patterns.md | 20 ++++++++++++++++ .../language-reference/pattern-matching.md | 9 +++++++ .../component-design-guidelines.md | 24 ++++++++++++++++--- docs/fsharp/style-guide/conventions.md | 15 ++++++++++-- 4 files changed, 63 insertions(+), 5 deletions(-) diff --git a/docs/fsharp/language-reference/active-patterns.md b/docs/fsharp/language-reference/active-patterns.md index cbc8ae04de099..89560bb4deb89 100644 --- a/docs/fsharp/language-reference/active-patterns.md +++ b/docs/fsharp/language-reference/active-patterns.md @@ -177,6 +177,26 @@ let (|Int|_|) str = The attribute must be specified, because the use of a struct return is not inferred from simply changing the return type to `ValueOption`. For more information, see [RFC FS-1039](https://github.com/fsharp/fslang-design/blob/main/FSharp-6.0/FS-1039-struct-representation-for-active-patterns.md). +## Null active patterns + +In F# 9, nullability related active patterns were added. + +The first one is `| Null | NonNull x |`, which is a recommended way to handle possible nulls. In the following example, parameter `s` is inferred nullable via this active pattern usage: + +```fsharp + let len s = + match s with + | Null -> -1 + | NonNull s -> String.length s +``` + +If you rather want to automatically throw `NullReferenceException`, you can use the `| NonNullQuick |` pattern: + +```fsharp +let len (NonNullQuick str) = + String.length str // throws if the argument is null +``` + ## See also - [F# Language Reference](index.md) diff --git a/docs/fsharp/language-reference/pattern-matching.md b/docs/fsharp/language-reference/pattern-matching.md index d2a25a0334878..4ac2d0e7c6a87 100644 --- a/docs/fsharp/language-reference/pattern-matching.md +++ b/docs/fsharp/language-reference/pattern-matching.md @@ -219,6 +219,15 @@ let len (str: string | null) = | s -> s.Length ``` +Similarly, you can use new dedicated nullability related [patterns](./active-patterns.md): + +```fsharp +let let str = // str is inferred to be `string | null` + match str with + | Null -> -1 + | NonNull (s: string) -> s.Length +``` + ## Nameof pattern The `nameof` pattern matches against a string when its value is equal to the expression that follows the `nameof` keyword. for example: diff --git a/docs/fsharp/style-guide/component-design-guidelines.md b/docs/fsharp/style-guide/component-design-guidelines.md index 491681d0bfaac..123c3c1361f41 100644 --- a/docs/fsharp/style-guide/component-design-guidelines.md +++ b/docs/fsharp/style-guide/component-design-guidelines.md @@ -713,7 +713,7 @@ let checkNonNull' argName (arg: obj) = else () ``` -To address the warnings, you need to explicitly specify `null` as a possible argument value: +To address the warnings, you could explicitly specify `null` as a possible argument value: ```fsharp let checkNonNull argName (arg: obj | null) = @@ -726,6 +726,24 @@ let checkNonNull' argName (arg: obj | null) = else () ``` +Yet, the most succinct way to achieve this is to use the new builtin `nullArgCheck` function: + +```fsharp +let checkNonNull'' argName arg = // `arg` is inferred to be `obj | null` + nullArgCheck argName arg + |> ignore +``` + +The `nullArgCheck` function is also handy when you need to sanitize the input and keep it in case it's not null: + +```fsharp +let processNullableList l = + // `l` is inferred as `int list | null` + let l = nullArgCheck (nameof l) l + // now, `l` is inferred as `int list` and is safe to use + l |> List.map (fun x -> x + 1) +``` + On the other hand, you'll get a warning if the compiler detects that a possible null value is not handled: ```fsharp @@ -748,8 +766,8 @@ let printLineLength (s: string) = let readLineFromStream (sr: System.IO.StreamReader) = let line = sr.ReadLine() match line with - | null -> () - | s -> printLineLength s + | Null -> () + | NonNull s -> printLineLength s ``` #### Avoid using tuples as return values diff --git a/docs/fsharp/style-guide/conventions.md b/docs/fsharp/style-guide/conventions.md index 3498b4cb6b046..3e3a3dcdede28 100644 --- a/docs/fsharp/style-guide/conventions.md +++ b/docs/fsharp/style-guide/conventions.md @@ -716,8 +716,19 @@ let getLineFromStream (stream: System.IO.StreamReader) : string | null = ```fsharp let getLineFromStream (stream: System.IO.StreamReader) : string option = match stream.ReadLine() with - | null -> None - | s -> Some s + | Null -> None + | NonNull s -> Some s +``` + +For raising null related exceptions you can use special `nullArgCheck` and `nonNull` functions: + +```fsharp +let inline checkNonNull arg = + nullArgCheck (nameof arg) arg // throws `ArgumentNullException` + |> ignore + +let inline assertNonNull arg = + nonNull arg // throws `NullReferenceException` ``` ## Object programming From 414ab5dda9a388c592f84f54d666126dfb99aa58 Mon Sep 17 00:00:00 2001 From: Petr Date: Tue, 7 Jan 2025 16:36:37 +0100 Subject: [PATCH 05/15] Up --- .../language-reference/active-patterns.md | 2 +- .../language-reference/compiler-directives.md | 25 +++++++++++++++++++ .../language-reference/compiler-options.md | 1 + .../fsharp-interactive-options.md | 1 + .../language-reference/values/null-values.md | 2 ++ 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/fsharp/language-reference/active-patterns.md b/docs/fsharp/language-reference/active-patterns.md index 89560bb4deb89..0d40f495ba7b2 100644 --- a/docs/fsharp/language-reference/active-patterns.md +++ b/docs/fsharp/language-reference/active-patterns.md @@ -181,7 +181,7 @@ The attribute must be specified, because the use of a struct return is not infer In F# 9, nullability related active patterns were added. -The first one is `| Null | NonNull x |`, which is a recommended way to handle possible nulls. In the following example, parameter `s` is inferred nullable via this active pattern usage: +The first one is `| Null | NonNull x |`, which is a recommended way to handle possible nulls. In the following example, parameter `s` is inferred nullable via this active pattern usage: ```fsharp let len s = diff --git a/docs/fsharp/language-reference/compiler-directives.md b/docs/fsharp/language-reference/compiler-directives.md index a94b560561768..5edcc9ed55b36 100644 --- a/docs/fsharp/language-reference/compiler-directives.md +++ b/docs/fsharp/language-reference/compiler-directives.md @@ -52,6 +52,31 @@ let str = "Debugging!" #endif ``` +## NULLABLE directive + +Starting with F# 9, you can enable nullable reference types in the project: + +```xml +enable +``` + +This automatically sets `define:NULLABLE` directive to the build. It's useful while initially rolling out the feature, to conditionally change conflicting code by `#if NULLABLE` hash directives: + +```fsharp +#if NULLABLE +let checkNonNull argName (arg: obj) = + match arg with + // nullness warning: the type 'obj' does not support 'null' + | null -> nullArg argName + | _ -> () +#else +let checkNonNull argName (arg: obj) = + match arg with + | null -> nullArg argName + | _ -> () +#endif +``` + ## Line Directives When building, the compiler reports errors in F# code by referencing line numbers on which each error occurs. These line numbers start at 1 for the first line in a file. However, if you are generating F# source code from another tool, the line numbers in the generated code are generally not of interest, because the errors in the generated F# code most likely arise from another source. The `#line` directive provides a way for authors of tools that generate F# source code to pass information about the original line numbers and source files to the generated F# code. diff --git a/docs/fsharp/language-reference/compiler-options.md b/docs/fsharp/language-reference/compiler-options.md index ef5f3bc91fc0b..e25ef30c96b9f 100644 --- a/docs/fsharp/language-reference/compiler-options.md +++ b/docs/fsharp/language-reference/compiler-options.md @@ -18,6 +18,7 @@ The following table shows compiler options listed alphabetically. Some of the F# |`--allsigs`|Generates a new (or regenerates an existing) signature file for each source file in the compilation. For more information about signature files, see [Signatures](signature-files.md).| |`-a filename.fs`|Generates a library from the specified file. This option is a short form of `--target:library filename.fs`.| |`--baseaddress:address`|Specifies the preferred base address at which to load a DLL.

This compiler option is equivalent to the C# compiler option of the same name. For more information, see [/baseaddress (C# Compiler Options)](../../csharp/language-reference/compiler-options/advanced.md#baseaddress).| +|--checknulls[+|-]|Enables [nullable reference types](./values/null-values.md#null-values-starting-with-f-9), added in F# 9.| |`--codepage:id`|Specifies which code page to use during compilation if the required page isn't the current default code page for the system.

This compiler option is equivalent to the C# compiler option of the same name. For more information, see [/code pages (C# Compiler Options)](../../csharp/language-reference/compiler-options/advanced.md#codepage).| |`--consolecolors`|Specifies that errors and warnings use color-coded text on the console.| |`--crossoptimize[+ or -]`|Enables or disables cross-module optimizations.| diff --git a/docs/fsharp/language-reference/fsharp-interactive-options.md b/docs/fsharp/language-reference/fsharp-interactive-options.md index 339af09765c7c..85bed67294c7d 100644 --- a/docs/fsharp/language-reference/fsharp-interactive-options.md +++ b/docs/fsharp/language-reference/fsharp-interactive-options.md @@ -27,6 +27,7 @@ Where lists appear in F# Interactive option arguments, list elements are separat |------|-----------| |**--**|Used to instruct F# Interactive to treat remaining arguments as command-line arguments to the F# program or script, which you can access in code by using the list **fsi.CommandLineArgs**.| |**--checked**[**+**|**-**]|Same as the **fsc.exe** compiler option. For more information, see [Compiler Options](compiler-options.md).| +|**--checknulls**[**+**|**-**]|Same as the **fsc.exe** compiler option. For more information, see [Compiler Options](compiler-options.md).| |**--codepage:<int>**|Same as the **fsc.exe** compiler option. For more information, see [Compiler Options](compiler-options.md).| |**--consolecolors**[**+**|**-**]|Outputs warning and error messages in color.| |**--compilertool:<extensionsfolder>|Reference an assembly or directory containing a design time tool (Short form: -t).| diff --git a/docs/fsharp/language-reference/values/null-values.md b/docs/fsharp/language-reference/values/null-values.md index 82f91b1d9760a..f11ffb3559ef4 100644 --- a/docs/fsharp/language-reference/values/null-values.md +++ b/docs/fsharp/language-reference/values/null-values.md @@ -39,6 +39,8 @@ In F# 9, extra capabilities are added to the language to deal with reference typ enable ``` +This passes the `--checknulls+` [flag](../compiler-options.md) to the F# compiler and sets a `define:NULLABLE` [preprocessor directive](../compiler-directives.md#nullable-directive) for the build. + To explicitly opt-in into nullability, a type declaration has to be suffixed with the new syntax: ```fsharp From 6ed04246d502403647aa1d64d97c4bfb94ec4589 Mon Sep 17 00:00:00 2001 From: Petr Date: Thu, 16 Jan 2025 14:02:45 +0100 Subject: [PATCH 06/15] up --- docs/fsharp/language-reference/active-patterns.md | 4 ++-- docs/fsharp/language-reference/compiler-directives.md | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/fsharp/language-reference/active-patterns.md b/docs/fsharp/language-reference/active-patterns.md index 0d40f495ba7b2..d7960e5c975b7 100644 --- a/docs/fsharp/language-reference/active-patterns.md +++ b/docs/fsharp/language-reference/active-patterns.md @@ -193,8 +193,8 @@ The first one is `| Null | NonNull x |`, which is a recommended way to handle po If you rather want to automatically throw `NullReferenceException`, you can use the `| NonNullQuick |` pattern: ```fsharp -let len (NonNullQuick str) = - String.length str // throws if the argument is null +let len (NonNullQuick str) = // throws if the argument is null + String.length str ``` ## See also diff --git a/docs/fsharp/language-reference/compiler-directives.md b/docs/fsharp/language-reference/compiler-directives.md index 5edcc9ed55b36..be3ce458f08fb 100644 --- a/docs/fsharp/language-reference/compiler-directives.md +++ b/docs/fsharp/language-reference/compiler-directives.md @@ -64,9 +64,8 @@ This automatically sets `define:NULLABLE` directive to the build. It's useful wh ```fsharp #if NULLABLE -let checkNonNull argName (arg: obj) = +let checkNonNull argName (arg: obj | null) = match arg with - // nullness warning: the type 'obj' does not support 'null' | null -> nullArg argName | _ -> () #else From 32a76a48a446cf08a1a13c50c925e961824aafdf Mon Sep 17 00:00:00 2001 From: Petr Date: Thu, 16 Jan 2025 14:15:23 +0100 Subject: [PATCH 07/15] Update docs/fsharp/language-reference/generics/constraints.md Co-authored-by: Tomas Grosup --- docs/fsharp/language-reference/generics/constraints.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fsharp/language-reference/generics/constraints.md b/docs/fsharp/language-reference/generics/constraints.md index a0e222038400e..19694bdb76a14 100644 --- a/docs/fsharp/language-reference/generics/constraints.md +++ b/docs/fsharp/language-reference/generics/constraints.md @@ -21,7 +21,7 @@ There are several different constraints you can apply to limit the types that ca |----------|------|-----------| |Type Constraint|*type-parameter* :> *type*|The provided type must be equal to or derived from the type specified, or, if the type is an interface, the provided type must implement the interface.| |Null Constraint|*type-parameter* : null|The provided type must support the null literal. This includes all .NET object types but not F# list, tuple, function, class, record, or union types.| -|Not Null Constraint|*type-parameter* : not null|The provided type must not support the null literal. This disallows both `null` annotated types and types which have null as their representation value (such as the option type). This does allow value types, since those can never be null.| +|Not Null Constraint|*type-parameter* : not null|The provided type must not support the null value. This disallows both `null` annotated types and types which have null as their representation value (such as the option type or types defined with AllowNullLiteral attribute). This generic constraint does allow value types, since those can never be null.| |Explicit Member Constraint|[(]*type-parameter* [or ... or *type-parameter*)] : (*member-signature*)|At least one of the type arguments provided must have a member that has the specified signature; not intended for common use. Members must be either explicitly defined on the type or part of an implicit type extension to be valid targets for an Explicit Member Constraint.| |Constructor Constraint|*type-parameter* : ( new : unit -> 'a )|The provided type must have a parameterless constructor.| |Value Type Constraint|*type-parameter* : struct|The provided type must be a .NET value type.| From 7eaf861a96dc7059767c6087f4b204835c3bff66 Mon Sep 17 00:00:00 2001 From: Petr Date: Thu, 16 Jan 2025 14:15:49 +0100 Subject: [PATCH 08/15] Up --- docs/fsharp/language-reference/compiler-directives.md | 2 +- docs/fsharp/language-reference/generics/constraints.md | 4 ++-- docs/fsharp/language-reference/values/null-values.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/fsharp/language-reference/compiler-directives.md b/docs/fsharp/language-reference/compiler-directives.md index be3ce458f08fb..7f1effc8d18c7 100644 --- a/docs/fsharp/language-reference/compiler-directives.md +++ b/docs/fsharp/language-reference/compiler-directives.md @@ -60,7 +60,7 @@ Starting with F# 9, you can enable nullable reference types in the project: enable ``` -This automatically sets `define:NULLABLE` directive to the build. It's useful while initially rolling out the feature, to conditionally change conflicting code by `#if NULLABLE` hash directives: +This automatically sets `NULLABLE` directive to the build. It's useful while initially rolling out the feature, to conditionally change conflicting code by `#if NULLABLE` hash directives: ```fsharp #if NULLABLE diff --git a/docs/fsharp/language-reference/generics/constraints.md b/docs/fsharp/language-reference/generics/constraints.md index 19694bdb76a14..0aa7683b52a7f 100644 --- a/docs/fsharp/language-reference/generics/constraints.md +++ b/docs/fsharp/language-reference/generics/constraints.md @@ -20,7 +20,7 @@ There are several different constraints you can apply to limit the types that ca |Constraint|Syntax|Description| |----------|------|-----------| |Type Constraint|*type-parameter* :> *type*|The provided type must be equal to or derived from the type specified, or, if the type is an interface, the provided type must implement the interface.| -|Null Constraint|*type-parameter* : null|The provided type must support the null literal. This includes all .NET object types but not F# list, tuple, function, class, record, or union types.| +|Null Constraint|*type-parameter* : null|The provided type must support the null value. This includes all .NET object types but not F# list, tuple, function, class, record, or union types.| |Not Null Constraint|*type-parameter* : not null|The provided type must not support the null value. This disallows both `null` annotated types and types which have null as their representation value (such as the option type or types defined with AllowNullLiteral attribute). This generic constraint does allow value types, since those can never be null.| |Explicit Member Constraint|[(]*type-parameter* [or ... or *type-parameter*)] : (*member-signature*)|At least one of the type arguments provided must have a member that has the specified signature; not intended for common use. Members must be either explicitly defined on the type or part of an implicit type extension to be valid targets for an Explicit Member Constraint.| |Constructor Constraint|*type-parameter* : ( new : unit -> 'a )|The provided type must have a parameterless constructor.| @@ -56,7 +56,7 @@ type Class3<'T when 'T : null> = class end // Not Null constraint -type Class3<'T when 'T : not null> = +type Class4<'T when 'T : not null> = class end // Member constraint with instance member diff --git a/docs/fsharp/language-reference/values/null-values.md b/docs/fsharp/language-reference/values/null-values.md index f11ffb3559ef4..0c6abb9152620 100644 --- a/docs/fsharp/language-reference/values/null-values.md +++ b/docs/fsharp/language-reference/values/null-values.md @@ -39,7 +39,7 @@ In F# 9, extra capabilities are added to the language to deal with reference typ enable ``` -This passes the `--checknulls+` [flag](../compiler-options.md) to the F# compiler and sets a `define:NULLABLE` [preprocessor directive](../compiler-directives.md#nullable-directive) for the build. +This passes the `--checknulls+` [flag](../compiler-options.md) to the F# compiler and sets a `NULLABLE` [preprocessor directive](../compiler-directives.md#nullable-directive) for the build. To explicitly opt-in into nullability, a type declaration has to be suffixed with the new syntax: From d66ceeec4c73f72487a1cf16b9f8649272c6defd Mon Sep 17 00:00:00 2001 From: Petr Date: Thu, 16 Jan 2025 15:30:01 +0100 Subject: [PATCH 09/15] Update component-design-guidelines.md --- .../component-design-guidelines.md | 42 +++---------------- 1 file changed, 5 insertions(+), 37 deletions(-) diff --git a/docs/fsharp/style-guide/component-design-guidelines.md b/docs/fsharp/style-guide/component-design-guidelines.md index 123c3c1361f41..3152faea8ce17 100644 --- a/docs/fsharp/style-guide/component-design-guidelines.md +++ b/docs/fsharp/style-guide/component-design-guidelines.md @@ -698,22 +698,7 @@ let checkNonNull' argName (arg: obj) = else () ``` -Starting with F# 9, you can leverage the new `| null` [syntax](../language-reference/values/null-values.md#null-values-starting-with-f-9) to make the compiler indicate possible null values and where they need handling. The above code will then start producing warnings: - -```fsharp -let checkNonNull argName (arg: obj) = - match arg with - // nullness warning: the type 'obj' does not support 'null' - | null -> nullArg argName - | _ -> () - -let checkNonNull' argName (arg: obj) = - // nullness warning: the type 'obj' does not support 'null' - if isNull arg then nullArg argName - else () -``` - -To address the warnings, you could explicitly specify `null` as a possible argument value: +Starting with F# 9, you can leverage the new `| null` [syntax](../language-reference/values/null-values.md#null-values-starting-with-f-9) to make the compiler indicate possible null values and where they need handling: ```fsharp let checkNonNull argName (arg: obj | null) = @@ -726,31 +711,14 @@ let checkNonNull' argName (arg: obj | null) = else () ``` -Yet, the most succinct way to achieve this is to use the new builtin `nullArgCheck` function: - -```fsharp -let checkNonNull'' argName arg = // `arg` is inferred to be `obj | null` - nullArgCheck argName arg - |> ignore -``` - -The `nullArgCheck` function is also handy when you need to sanitize the input and keep it in case it's not null: - -```fsharp -let processNullableList l = - // `l` is inferred as `int list | null` - let l = nullArgCheck (nameof l) l - // now, `l` is inferred as `int list` and is safe to use - l |> List.map (fun x -> x + 1) -``` - -On the other hand, you'll get a warning if the compiler detects that a possible null value is not handled: +In F# 9, the compiler emits a warning when it detects that a possible null value is not handled: ```fsharp let printLineLength (s: string) = printfn "%i" s.Length let readLineFromStream (sr: System.IO.StreamReader) = + // `ReadLine` may return null here - when the stream is finished let line = sr.ReadLine() // nullness warning: The types 'string' and 'string | null' // do not have equivalent nullability @@ -766,8 +734,8 @@ let printLineLength (s: string) = let readLineFromStream (sr: System.IO.StreamReader) = let line = sr.ReadLine() match line with - | Null -> () - | NonNull s -> printLineLength s + | null -> () + | s -> printLineLength s ``` #### Avoid using tuples as return values From cbdfdb71e4e7616fcd1e409dc59531c37030fbbf Mon Sep 17 00:00:00 2001 From: Petr Date: Thu, 16 Jan 2025 16:13:42 +0100 Subject: [PATCH 10/15] up --- docs/fsharp/style-guide/conventions.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/fsharp/style-guide/conventions.md b/docs/fsharp/style-guide/conventions.md index 3e3a3dcdede28..7e6577feee1dc 100644 --- a/docs/fsharp/style-guide/conventions.md +++ b/docs/fsharp/style-guide/conventions.md @@ -714,21 +714,22 @@ let getLineFromStream (stream: System.IO.StreamReader) : string | null = **Instead**, use idiomatic F# means (e.g., options): ```fsharp -let getLineFromStream (stream: System.IO.StreamReader) : string option = - match stream.ReadLine() with - | Null -> None - | NonNull s -> Some s +let getLineFromStream (stream: System.IO.StreamReader) = + stream.ReadLine() |> Option.ofObj ``` -For raising null related exceptions you can use special `nullArgCheck` and `nonNull` functions: +For raising null related exceptions you can use special `nullArgCheck` and `nonNull` functions. They're handy also because in case the value is not null, they [shadow](../language-reference/functions/index.md#scope) the argument with its sanitized value - the further code cannot access possible null pointers anymore. ```fsharp -let inline checkNonNull arg = - nullArgCheck (nameof arg) arg // throws `ArgumentNullException` - |> ignore +let inline processNullableList list = + let list = nullArgCheck (nameof list) list // throws `ArgumentNullException` + // 'list' is safe to use from now on + list |> List.distinct -let inline assertNonNull arg = - nonNull arg // throws `NullReferenceException` +let inline processNullableList' list = + let list = nonNull list // throws `NullReferenceException` + // 'list' is safe to use from now on + list |> List.distinct ``` ## Object programming From 2753624de24c3ca548f84b6ef8c96cb6f8e52faf Mon Sep 17 00:00:00 2001 From: Petr Date: Thu, 16 Jan 2025 16:37:56 +0100 Subject: [PATCH 11/15] up --- .../language-reference/compiler-directives.md | 12 +++++------- docs/fsharp/style-guide/conventions.md | 14 +++++--------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/docs/fsharp/language-reference/compiler-directives.md b/docs/fsharp/language-reference/compiler-directives.md index 7f1effc8d18c7..b467d5339cf6d 100644 --- a/docs/fsharp/language-reference/compiler-directives.md +++ b/docs/fsharp/language-reference/compiler-directives.md @@ -64,15 +64,13 @@ This automatically sets `NULLABLE` directive to the build. It's useful while ini ```fsharp #if NULLABLE -let checkNonNull argName (arg: obj | null) = - match arg with - | null -> nullArg argName - | _ -> () +let length (arg: 'T when 'T: not null) = + Seq.length arg #else -let checkNonNull argName (arg: obj) = +let length arg = match arg with - | null -> nullArg argName - | _ -> () + | null -> -1 + | s -> Seq.length s #endif ``` diff --git a/docs/fsharp/style-guide/conventions.md b/docs/fsharp/style-guide/conventions.md index 7e6577feee1dc..24473ed2c06f1 100644 --- a/docs/fsharp/style-guide/conventions.md +++ b/docs/fsharp/style-guide/conventions.md @@ -689,19 +689,15 @@ For legacy reasons some string functions in FSharp.Core still treat nulls as emp ### Leverage F# 9 null syntax at the API boundaries -F# 9 adds [syntax](../language-reference/values/null-values.md#null-values-starting-with-f-9) to explicitly state that a value can be null. It's designed to be used on the API boundaries, to make the compiler indicate the places where null handling null is missing. +F# 9 adds [syntax](../language-reference/values/null-values.md#null-values-starting-with-f-9) to explicitly state that a value can be null. It's designed to be used on the API boundaries, to make the compiler indicate the places where null handling is missing. Here is an example of the valid usage of the syntax: ```fsharp -let processStream (stream: System.IO.StreamReader) = - let processLine (line: string | null) = - match line with - | null -> (); false - | s -> printfn "%s" s; true - - while processLine(stream.ReadLine()) do () - stream.Close() +let processLineFromStream (line: string | null) = + match line with + | null -> (); false + | s -> printfn "%s" s; true ``` **Avoid** propagating nulls further down your F# code: From 9ef15e24f668cf68a4068bc94ca1540a6a77e60c Mon Sep 17 00:00:00 2001 From: Petr Date: Thu, 16 Jan 2025 16:55:31 +0100 Subject: [PATCH 12/15] Update null-values.md --- docs/fsharp/language-reference/values/null-values.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fsharp/language-reference/values/null-values.md b/docs/fsharp/language-reference/values/null-values.md index 0c6abb9152620..08a708e287158 100644 --- a/docs/fsharp/language-reference/values/null-values.md +++ b/docs/fsharp/language-reference/values/null-values.md @@ -95,7 +95,7 @@ match x with | null -> ... ``` -Note that the extra null related capabilities were added to the language for the interoperability purposes. Using `| null` within F# code is not considered idiomatic for denoting missing information - for that purpose, use options (as described above). Read more about null-related [conventions](../../style-guide/conventions.md#nulls-and-default-values) in the style guide. +Note that the extra null related capabilities were added to the language for the interoperability purposes. Using `| null` in F# type modeling is not considered idiomatic for denoting missing information - for that purpose, use options (as described above). Read more about null-related [conventions](../../style-guide/conventions.md#nulls-and-default-values) in the style guide. ## See also From 99c4b7dfd1844c5c5bc0f0d4556645b66d841c25 Mon Sep 17 00:00:00 2001 From: Petr Date: Tue, 21 Jan 2025 14:00:26 +0100 Subject: [PATCH 13/15] Update docs/fsharp/style-guide/component-design-guidelines.md Co-authored-by: Tomas Grosup --- docs/fsharp/style-guide/component-design-guidelines.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fsharp/style-guide/component-design-guidelines.md b/docs/fsharp/style-guide/component-design-guidelines.md index 3152faea8ce17..ad0a369492aba 100644 --- a/docs/fsharp/style-guide/component-design-guidelines.md +++ b/docs/fsharp/style-guide/component-design-guidelines.md @@ -725,7 +725,7 @@ let readLineFromStream (sr: System.IO.StreamReader) = printLineLength line ``` -These warnings should be addressed using idiomatic F# [null pattern](../language-reference/pattern-matching.md#null-pattern) via pattern matching: +These warnings should be addressed using F# [null pattern](../language-reference/pattern-matching.md#null-pattern) in matching: ```fsharp let printLineLength (s: string) = From 29aaced0eb9b641a18d414489eb76064b8599bcf Mon Sep 17 00:00:00 2001 From: Petr Date: Tue, 21 Jan 2025 15:11:23 +0100 Subject: [PATCH 14/15] Update conventions.md --- docs/fsharp/style-guide/conventions.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/fsharp/style-guide/conventions.md b/docs/fsharp/style-guide/conventions.md index 24473ed2c06f1..ab4df74e63819 100644 --- a/docs/fsharp/style-guide/conventions.md +++ b/docs/fsharp/style-guide/conventions.md @@ -694,10 +694,17 @@ F# 9 adds [syntax](../language-reference/values/null-values.md#null-values-start Here is an example of the valid usage of the syntax: ```fsharp -let processLineFromStream (line: string | null) = - match line with - | null -> (); false - | s -> printfn "%s" s; true +type CustomType(m1, m2) = + member _.M1 = m1 + member _.M2 = m2 + + override this.Equals(obj: obj | null) = + match obj with + | :? CustomType as other -> this.M1 = other.M1 && this.M2 = other.M2 + | _ -> false + + override this.GetHashCode() = + hash (this.M1, this.M2) ``` **Avoid** propagating nulls further down your F# code: From 5d57069da7b301eeb9e6ebb99ec4d9604d4a37b3 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 22 Jan 2025 13:20:02 -0500 Subject: [PATCH 15/15] Update docs/fsharp/language-reference/values/null-values.md --- docs/fsharp/language-reference/values/null-values.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/fsharp/language-reference/values/null-values.md b/docs/fsharp/language-reference/values/null-values.md index 08a708e287158..ff99f5a7ec3d7 100644 --- a/docs/fsharp/language-reference/values/null-values.md +++ b/docs/fsharp/language-reference/values/null-values.md @@ -95,7 +95,8 @@ match x with | null -> ... ``` -Note that the extra null related capabilities were added to the language for the interoperability purposes. Using `| null` in F# type modeling is not considered idiomatic for denoting missing information - for that purpose, use options (as described above). Read more about null-related [conventions](../../style-guide/conventions.md#nulls-and-default-values) in the style guide. +> [!IMPORTANT] +> The extra null related capabilities were added to the language for the interoperability purposes. Using `| null` in F# type modeling is not considered idiomatic for denoting missing information - for that purpose, use options (as described above). Read more about null-related [conventions](../../style-guide/conventions.md#nulls-and-default-values) in the style guide. ## See also