dotnet / fsharp

The F# compiler, F# core library, F# language service, and F# tooling integration for Visual Studio
https://dotnet.microsoft.com/languages/fsharp
MIT License
3.92k stars 785 forks source link

Nullness issue - Additional type hints needed in Computation Expression #17776

Open marklam opened 1 month ago

marklam commented 1 month ago

Issue description

I found a couple of examples where type hints are needed inside the FsToolkit.ErrorHandling option CE with `--checknulls`` turned on.

I'm providing some example code, and comments where the type hints need to be added. This compiles without the extra hints when `--checknulls`` is not specified.

Choose one or more from the following categories of impact

Operating System

Windows (Default)

What .NET runtime/SDK kind are you seeing the issue on

.NET SDK (.NET Core, .NET 5+)

.NET Runtime/SDK version

9.0.100-rc.1.24452.12

Reproducible code snippet and actual behavior

module Program

open FSharp.UMX
open FsToolkit.ErrorHandling

type [<Measure>] m
type [<Measure>] r

type P = | P
type S = | S

let doThing (a: float<m>) (b:float<m>) =
    ()

let x (mr : (float<m>*float<r>) option) : unit =
    option {
        // Needs annotation
        //let! (m:float<m>,r:float<r>) = mr
        let! (m, r) = mr

        let r = UMX.cast<r, m>r
        doThing r m
    }
    |> ignore

let y (s : S option ) (p : P option)=
    option {
        // Needs annotation
        //let! (_s : S) = s
        let! _s = s

        // Needs annotation
        //let! (_p : P) = p
        let! _p = p

        ()
    } |> ignore

[<EntryPoint>]
let main argv =
    0
  Nullness failed with 3 error(s) (0.9s)
    D:\Git\Temp\Nullness\Nullness\Program.fs(21,17): error FS0041: A unique overload for method 'cast' could not be determined based on type information prior to this program point. A type annotation may be needed.Known type of argument: 'aCandidates: - static member UMX.cast: x: DateTime<'m1> -> DateTime<'m2> - static member UMX.cast: x: DateTimeOffset<'m1> -> DateTimeOffset<'m2> - static member UMX.cast: x: Guid<'m1> -> Guid<'m2> - static member UMX.cast: x: TimeSpan<'m1> -> TimeSpan<'m2> - static member UMX.cast: x: bool<'m1> -> bool<'m2> - static member UMX.cast: x: byte<'m1> -> byte<'m2> - static member UMX.cast: x: decimal<'m1> -> decimal<'m2> - static member UMX.cast: x: float32<'m1> -> float32<'m2> - static member UMX.cast: x: float<'m1> -> float<'m2> - static member UMX.cast: x: int16<'m1> -> int16<'m2> - static member UMX.cast: x: int64<'m1> -> int64<'m2> - static member UMX.cast: x: int<'m1> -> int<'m2> - static member UMX.cast: x: string<'m1> -> string<'m2> - static member UMX.cast: x: uint64<'m1> -> uint64<'m2>
    D:\Git\Temp\Nullness\Nullness\Program.fs(34,9): error FS0041: A unique overload for method 'Bind' could not be determined based on type information prior to this program point. A type annotation may be needed.Known types of arguments: P option * ('a -> unit option)Candidates: - member OptionBuilder.Bind: input: 'input option * [<InlineIfLambda>] binder: ('input -> 'output option) -> 'output option - member OptionBuilder.Bind: m: 'input * [<InlineIfLambda>] binder: ('input -> 'output option) -> 'output option when 'input: null
    D:\Git\Temp\Nullness\Nullness\Program.fs(30,9): error FS0041: A unique overload for method 'Bind' could not be determined based on type information prior to this program point. A type annotation may be needed.Known types of arguments: S option * ('a -> 'b)Candidates: - member OptionBuilder.Bind: input: 'input option * [<InlineIfLambda>] binder: ('input -> 'output option) -> 'output option - member OptionBuilder.Bind: m: 'input * [<InlineIfLambda>] binder: ('input -> 'output option) -> 'output option when 'input: null

Possible workarounds

No response

marklam commented 1 month ago

Added a repo with the code https://github.com/marklam/Nullness

T-Gro commented 1 month ago

I will have a look.

A potential resolution, at least untill FsToolit.ErrorHandling is updated to make use of NRTs, is to offer an alternative option builder which would understand nullable reference types and offer non conflicting overloads.

The pre-nullness builder can bind either from option, or a from a type which was potentially nullable (the null generic constrain). With NRTs and full C#-interop, option must be considered as nullable too, because None is represented as null at runtime (and what interop gets to see). Which leads to a conflict of the two overloads, because both are possible.

This is also an example where a "betterness" resolution of overloads would be good (cc @vzarytovskii - a specific option overload winning over a generic one with a constraint), F# does not have it now.