fsharp icon indicating copy to clipboard operation
fsharp copied to clipboard

Nullness issue - Additional type hints needed in Computation Expression

Open marklam opened this issue 1 year ago • 2 comments

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

  • [ ] Unexpected nullness warning (false positive in nullness checking, code uses --checknulls and langversion:preview).
  • [ ] Missing nullness warning in a case which can produce nulls (false negative, code uses --checknulls and langversion:preview).
  • [ ] Breaking change related to older null constructs in code not using the checknulls switch.
  • [ ] Breaking change related to generic code and explicit type constraints (null, not null).
  • [X] Type inference issue (i.e. code worked without type annotations before, and applying the --checknulls enforces type annotations).
  • [ ] C#/F# interop issue related to nullness metadata.
  • [ ] Other (none of the categories above apply).

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 avatar Sep 21 '24 11:09 marklam

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

marklam avatar Sep 21 '24 11:09 marklam

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.

T-Gro avatar Sep 23 '24 09:09 T-Gro