Skip to content

Commit 5452826

Browse files
psfinakiT-GroBillWagner
authored
Nullable reference types in F# (#44047)
* Nullable reference types in F# * up * up * updates * Up * up * Update docs/fsharp/language-reference/generics/constraints.md Co-authored-by: Tomas Grosup <[email protected]> * Up * Update component-design-guidelines.md * up * up * Update null-values.md * Update docs/fsharp/style-guide/component-design-guidelines.md Co-authored-by: Tomas Grosup <[email protected]> * Update conventions.md * Update docs/fsharp/language-reference/values/null-values.md --------- Co-authored-by: Tomas Grosup <[email protected]> Co-authored-by: Bill Wagner <[email protected]>
1 parent e17b19e commit 5452826

File tree

9 files changed

+225
-3
lines changed

9 files changed

+225
-3
lines changed

docs/fsharp/language-reference/active-patterns.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,26 @@ let (|Int|_|) str =
177177

178178
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).
179179

180+
## Null active patterns
181+
182+
In F# 9, nullability related active patterns were added.
183+
184+
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:
185+
186+
```fsharp
187+
let len s =
188+
match s with
189+
| Null -> -1
190+
| NonNull s -> String.length s
191+
```
192+
193+
If you rather want to automatically throw `NullReferenceException`, you can use the `| NonNullQuick |` pattern:
194+
195+
```fsharp
196+
let len (NonNullQuick str) = // throws if the argument is null
197+
String.length str
198+
```
199+
180200
## See also
181201

182202
- [F# Language Reference](index.md)

docs/fsharp/language-reference/compiler-directives.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,28 @@ let str = "Debugging!"
5252
#endif
5353
```
5454

55+
## NULLABLE directive
56+
57+
Starting with F# 9, you can enable nullable reference types in the project:
58+
59+
```xml
60+
<Nullable>enable</Nullable>
61+
```
62+
63+
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:
64+
65+
```fsharp
66+
#if NULLABLE
67+
let length (arg: 'T when 'T: not null) =
68+
Seq.length arg
69+
#else
70+
let length arg =
71+
match arg with
72+
| null -> -1
73+
| s -> Seq.length s
74+
#endif
75+
```
76+
5577
## Line Directives
5678

5779
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.

docs/fsharp/language-reference/compiler-options.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ The following table shows compiler options listed alphabetically. Some of the F#
1818
|`--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).|
1919
|`-a filename.fs`|Generates a library from the specified file. This option is a short form of `--target:library filename.fs`.|
2020
|`--baseaddress:address`|Specifies the preferred base address at which to load a DLL.<br /><br />This compiler option is equivalent to the C# compiler option of the same name. For more information, see [&#47;baseaddress &#40;C&#35; Compiler Options&#41;](../../csharp/language-reference/compiler-options/advanced.md#baseaddress).|
21+
|<code>--checknulls[+&#124;-]</code>|Enables [nullable reference types](./values/null-values.md#null-values-starting-with-f-9), added in F# 9.|
2122
|`--codepage:id`|Specifies which code page to use during compilation if the required page isn't the current default code page for the system.<br /><br />This compiler option is equivalent to the C# compiler option of the same name. For more information, see [&#47;code pages &#40;C&#35; Compiler Options&#41;](../../csharp/language-reference/compiler-options/advanced.md#codepage).|
2223
|`--consolecolors`|Specifies that errors and warnings use color-coded text on the console.|
2324
|`--crossoptimize[+ or -]`|Enables or disables cross-module optimizations.|

docs/fsharp/language-reference/fsharp-interactive-options.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Where lists appear in F# Interactive option arguments, list elements are separat
2727
|------|-----------|
2828
|**--**|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**.|
2929
|**--checked**[**+**&#124;**-**]|Same as the **fsc.exe** compiler option. For more information, see [Compiler Options](compiler-options.md).|
30+
|**--checknulls**[**+**&#124;**-**]|Same as the **fsc.exe** compiler option. For more information, see [Compiler Options](compiler-options.md).|
3031
|**--codepage:&lt;int&gt;**|Same as the **fsc.exe** compiler option. For more information, see [Compiler Options](compiler-options.md).|
3132
|**--consolecolors**[**+**&#124;**-**]|Outputs warning and error messages in color.|
3233
|**--compilertool:&lt;extensionsfolder&gt;|Reference an assembly or directory containing a design time tool (Short form: -t).|

docs/fsharp/language-reference/generics/constraints.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ There are several different constraints you can apply to limit the types that ca
2020
|Constraint|Syntax|Description|
2121
|----------|------|-----------|
2222
|Type Constraint|*type-parameter* :&gt; *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.|
23-
|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.|
23+
|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.|
24+
|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.|
2425
|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.|
2526
|Constructor Constraint|*type-parameter* : ( new : unit -&gt; 'a )|The provided type must have a parameterless constructor.|
2627
|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> =
5455
type Class3<'T when 'T : null> =
5556
class end
5657
58+
// Not Null constraint
59+
type Class4<'T when 'T : not null> =
60+
class end
61+
5762
// Member constraint with instance member
5863
type Class5<'T when 'T : (member Method1 : 'T -> int)> =
5964
class end

docs/fsharp/language-reference/pattern-matching.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,24 @@ The following example uses the null pattern and the variable pattern.
210210

211211
[!code-fsharp[Main](~/samples/snippets/fsharp/lang-ref-2/snippet4817.fs)]
212212

213+
Null pattern is also recommended for the F# 9 [nullability capabilities](./values/null-values.md#null-values-starting-with-f-9):
214+
215+
```fsharp
216+
let len (str: string | null) =
217+
match str with
218+
| null -> -1
219+
| s -> s.Length
220+
```
221+
222+
Similarly, you can use new dedicated nullability related [patterns](./active-patterns.md):
223+
224+
```fsharp
225+
let let str = // str is inferred to be `string | null`
226+
match str with
227+
| Null -> -1
228+
| NonNull (s: string) -> s.Length
229+
```
230+
213231
## Nameof pattern
214232

215233
The `nameof` pattern matches against a string when its value is equal to the expression that follows the `nameof` keyword. for example:

docs/fsharp/language-reference/values/null-values.md

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ ms.date: 08/15/2020
77

88
This topic describes how the null value is used in F#.
99

10-
## Null Value
10+
## Null Values Prior To F# 9
1111

1212
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.
1313

@@ -31,6 +31,73 @@ You can use the following code to check if an arbitrary value is null.
3131

3232
[!code-fsharp[Main](~/samples/snippets/fsharp/lang-ref-1/snippet703.fs)]
3333

34+
## Null Values starting with F# 9
35+
36+
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:
37+
38+
```xml
39+
<Nullable>enable</Nullable>
40+
```
41+
42+
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.
43+
44+
To explicitly opt-in into nullability, a type declaration has to be suffixed with the new syntax:
45+
46+
```fsharp
47+
type | null
48+
```
49+
50+
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`.
51+
52+
The nullable annotation `| null` can be used at all places where a reference type would be normally used:
53+
54+
- Fields of union types, record types and custom types.
55+
- Type aliases to existing types.
56+
- Type applications of a generic type.
57+
- Explicit type annotations to let bindings, parameters or return types.
58+
- Type annotations to object-programming constructs like members, properties or fields.
59+
60+
```fsharp
61+
type AB = A | B
62+
type AbNull = AB | null
63+
64+
type RecordField = { X: string | null }
65+
type TupleField = string * string | null
66+
67+
type NestedGenerics = { Z : List<List<string | null> | null> | null }
68+
```
69+
70+
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:
71+
72+
```fsharp
73+
// Unexpected symbol '|' (directly before 'null') in member definition
74+
type DUField = N of string | null
75+
```
76+
77+
Wrapping the same type into a pair of `( )` parentheses fixes the issue:
78+
79+
```fsharp
80+
type DUField = N of (string | null)
81+
```
82+
83+
When used in pattern matching, `|` is used to separate different pattern matching clauses.
84+
85+
```fsharp
86+
match x with
87+
| ?: string | null -> ...
88+
```
89+
90+
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:
91+
92+
```fsharp
93+
match x with
94+
| ?: string
95+
| null -> ...
96+
```
97+
98+
> [!IMPORTANT]
99+
> 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.
100+
34101
## See also
35102

36103
- [Values](index.md)

docs/fsharp/style-guide/component-design-guidelines.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -693,11 +693,51 @@ let checkNonNull argName (arg: obj) =
693693
| null -> nullArg argName
694694
| _ -> ()
695695
696-
let checkNonNull` argName (arg: obj) =
696+
let checkNonNull' argName (arg: obj) =
697697
if isNull arg then nullArg argName
698698
else ()
699699
```
700700

701+
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:
702+
703+
```fsharp
704+
let checkNonNull argName (arg: obj | null) =
705+
match arg with
706+
| null -> nullArg argName
707+
| _ -> ()
708+
709+
let checkNonNull' argName (arg: obj | null) =
710+
if isNull arg then nullArg argName
711+
else ()
712+
```
713+
714+
In F# 9, the compiler emits a warning when it detects that a possible null value is not handled:
715+
716+
```fsharp
717+
let printLineLength (s: string) =
718+
printfn "%i" s.Length
719+
720+
let readLineFromStream (sr: System.IO.StreamReader) =
721+
// `ReadLine` may return null here - when the stream is finished
722+
let line = sr.ReadLine()
723+
// nullness warning: The types 'string' and 'string | null'
724+
// do not have equivalent nullability
725+
printLineLength line
726+
```
727+
728+
These warnings should be addressed using F# [null pattern](../language-reference/pattern-matching.md#null-pattern) in matching:
729+
730+
```fsharp
731+
let printLineLength (s: string) =
732+
printfn "%i" s.Length
733+
734+
let readLineFromStream (sr: System.IO.StreamReader) =
735+
let line = sr.ReadLine()
736+
match line with
737+
| null -> ()
738+
| s -> printLineLength s
739+
```
740+
701741
#### Avoid using tuples as return values
702742

703743
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.

docs/fsharp/style-guide/conventions.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,54 @@ module Array =
687687

688688
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".
689689

690+
### Leverage F# 9 null syntax at the API boundaries
691+
692+
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.
693+
694+
Here is an example of the valid usage of the syntax:
695+
696+
```fsharp
697+
type CustomType(m1, m2) =
698+
member _.M1 = m1
699+
member _.M2 = m2
700+
701+
override this.Equals(obj: obj | null) =
702+
match obj with
703+
| :? CustomType as other -> this.M1 = other.M1 && this.M2 = other.M2
704+
| _ -> false
705+
706+
override this.GetHashCode() =
707+
hash (this.M1, this.M2)
708+
```
709+
710+
**Avoid** propagating nulls further down your F# code:
711+
712+
```fsharp
713+
let getLineFromStream (stream: System.IO.StreamReader) : string | null =
714+
stream.ReadLine()
715+
```
716+
717+
**Instead**, use idiomatic F# means (e.g., options):
718+
719+
```fsharp
720+
let getLineFromStream (stream: System.IO.StreamReader) =
721+
stream.ReadLine() |> Option.ofObj
722+
```
723+
724+
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.
725+
726+
```fsharp
727+
let inline processNullableList list =
728+
let list = nullArgCheck (nameof list) list // throws `ArgumentNullException`
729+
// 'list' is safe to use from now on
730+
list |> List.distinct
731+
732+
let inline processNullableList' list =
733+
let list = nonNull list // throws `NullReferenceException`
734+
// 'list' is safe to use from now on
735+
list |> List.distinct
736+
```
737+
690738
## Object programming
691739

692740
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.

0 commit comments

Comments
 (0)