fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
345 stars 21 forks source link

Impove type inference for yields in computation expression to let them be aware of overloads with function parameters #1388

Open Lanayx opened 2 weeks ago

Lanayx commented 2 weeks ago

This suggestion is based on issue https://github.com/dotnet/fsharp/issues/17837

The following code is not currently allowed, it requires specifying int type for y explicitly.

open System

type Test() =
    member inline _.Yield(value: int -> unit) = ()
    member inline _.Yield(value: string) = () // without this overload issue is not observed
    member inline _.Zero() = ()

let x =
    Test() {
        yield (fun y -> Console.WriteLine(y)) // error, the type of y is unknown
    }

another case:

open System

type Person = {| Name: string |}

type Test() =
    member inline _.Yield(value: Person -> unit) = ()
    member inline _.Yield(value: string) = () // without this overload issue is not observed
    member inline _.Zero() = ()

let x =
    Test() {
        yield (fun y -> Console.WriteLine(y.Name)) // error, the type of y is unknown
    }

I suggest that this is undesired behavior and typechecker should be able to try function overload before giving up and saying that type is unknown. The suggested changes in typechecker by @T-gro:

-The callsite is known to be passing in a function (not yet fully resolved, but known to be a function) -There is exactly one overload with a function type argument (although I'm not sure why this is needed, I hope that typechecker can go through several overloads with function arguments and still resolve if there is only one match without error)

Pros and Cons

The advantage of making this adjustment to F# is less verbose code and consistent auto-inference behaviour.

The disadvantage of making this adjustment to F# is more work for typechecker

Extra information

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

Affidavit (please submit!)

Please tick these items by placing a cross in the box:

Please tick all that apply:

For Readers

If you would like to see this issue implemented, please click the :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.

vzarytovskii commented 2 weeks ago

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

It isn't, likely L

This is not only about yields in CE, this is more fundamental problem, we need to choose yield overload in this case, to do that we need to know the type of its argument, which we can't know because we can't typecheck it (we can't choose WriteLine overload, otherwise we would need to specialized it). In other words we can't infer type from specific Yield, because we don't know which one we want to use yet.

Let's say we make a change (special case the function somehow) to do type inference differently in certain cases, what happens when another overload is added?

open System

type Test() =
    member inline _.Yield(value: int -> unit) = ()
    member inline _.Yield(value: bool -> unit) = ()
    member inline _.Yield(value: string) = ()
    member inline _.Zero() = ()

let x =
    Test() {
        yield (fun y -> Console.WriteLine(y)) 
    }

Which overload should be chosen in this case, and which type should be inferred for y?

T-Gro commented 2 weeks ago

As soon as the "exactly one function overload" stops being true, it would go back to being a method resolution error needing additional type annotations.

This is not unlike other method resolution situations where adding a new overload causes a compilation error downstream (looking at BCL adding overloads).

Lanayx commented 2 weeks ago

Let's say we make a change (special case the function somehow) to do type inference differently in certain cases, what happens when another overload is added?

My understanding (as soon as I don't know current implementation details) are the following:

1st case: it's even simpler to me, since we have a finite set of possible overloads in Console.WriteLine and finite number of possible overloads for Yield. What I expect compiler to do is to find a valid intersection of two sets. If we get 0 matches - show an error, if we get more than 1 match - show an error (your question), if we get exactly 1 match then apply that.

2nd case is more complex to me, since there is no set of possible overloads, since the type is totally uknown, we only know that y should be of some record with field Name. In that case rather than just matching types compiler should look if whether if any of those is a record with field Name and then (just like in the first case) fail if there are 0 matches or more than one

It isn't, likely L

Will change, no problem

As soon as the "exactly one function overload" stops being true, it would go back to being a method resolution error needing additional type annotations.

As I said, my thoughts above are very naive since I don't know details of how typechecker implementation works today, so they easily might be impossible.