Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Most concrete" tiebreaker for generic overloads #905

Open
4 of 5 tasks
NinoFloris opened this issue Aug 25, 2020 · 8 comments
Open
4 of 5 tasks

"Most concrete" tiebreaker for generic overloads #905

NinoFloris opened this issue Aug 25, 2020 · 8 comments

Comments

@NinoFloris
Copy link

NinoFloris commented Aug 25, 2020

F# today has tiebreakers like intrinsic methods over extension methods and most derived over least derived type, yet it fails to address one - in my opinion - very common source of ambiguity errors.

That is the case where there are two overloads, like so

type Example =
    static member Invoke(value: Option<'t>) = ()
    static member Invoke(value: Option<List<'t>>) = ()

//error FS0041: A unique overload for method 'Invoke' could not be determined based on type information prior 
//to this program point. A type annotation may be needed. 
//Candidates: static member Example.Invoke : value:Option<'t> -> unit, static member Example.Invoke : value:Option<List<'t>> -> unit
let x = Example.Invoke(Some([1]))

See: sharplab

or like so (which I find more of an anti pattern than the one above but it does come up now and then)

open System
type Example =
    static member Invoke(value: 't) = ()
    static member Invoke(value: Option<'t>) = ()

//error FS0041: A unique overload for method 'Invoke' could not be determined based on type information prior
//to this program point. A type annotation may be needed. 
//Candidates: static member Example.Invoke : value:'t -> unit, static member Example.Invoke : value:Option<'t> -> unit     
let x = Example.Invoke(Some([1]))

I propose to add the 'obvious' tiebreaker of "most concrete"; meaning if a value is passed matching the shape of multiple generic methods choose the most concrete overload in the applicable set, in this case for Some [1] that would be Option<List<'t>>. When there are multiple arguments this should probably function just as the "most derived" tiebreaker does in the face of multiple matching overloads but the best decision here isn't entirely clear to me.

Some examples where workarounds for this are being employed:

One very common case of ambiguity errors in my own code is in the ValueTask<'T> constructors, where you must pass either a result: 'T or a task: Task<'T>, in this case disambiguation is (luckily) possible by naming the parameter but it's a frequent source of frustration.

There likely exists a large group of people that tried overloads like these but could not get things to work, at that point they either abandon it, or discover SRTPs which is arguably a heavy mechanism for these simple cases.

I expect there exists many more cases like these 'in the wild' and I encourage all readers to post them below. Additionally if you have tried to define generic overloads and ran into ambiguity errors please speak up as well.

With a large part of functional programming being about functors and generalization, generic overloads are not uncommon and you quickly bump into this shortcoming. I believe it's time overload resolution can properly deal with them.

Pros and Cons

Less hacks but additional complexity in overload mechanics.

Extra information

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

Related suggestions: (put links to related suggestions here)

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I or my company would be willing to help implement and/or test this

For Readers

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

@cmeeren
Copy link

cmeeren commented Aug 28, 2020

Some examples where workarounds for this are being employed:

I too have used this workaround in several of my projects, e.g. Felicity, FSharp.JsonApi, Feliz.MaterialUI, and Cvdm.ErrorHandling (now merged into FsToolkit.ErrorHandling), among several others. Having tie breakers as described here would make it notably easier to design flexible APIs.

@dsyme
Copy link
Collaborator

dsyme commented Jan 12, 2021

Yes this seems reasonable.

@En3Tho
Copy link

En3Tho commented Feb 17, 2022

This has bitten me many times, mostly in Interop scenarios.

I had to make my own ValueTask extensions (like mimicking Task's FromResult) and there are other types, coming from C# that have different overloads in constructors or methods and while calling one or two is not that much of a problem (in case of ValueTask you can just pick a right argument name, but at the same time not every constructor or method is like this, sometimes they share parameter name because why not, it's C#...) but when you have to build a more complex tree or a graph of objects it becomes a huge pain and code becomes quite unsightly or requires spending time writing workarounds just to build the thing...

CEs sometime are really hurt by this too.

@konst-sh
Copy link

Recently was trying code like this, and it tends to always resolve to other case, I assume PR would allow to handle this without reflections.

let castToOption (x: obj) =
    match x with
        | :? option<obj> as x1 -> ... // Do stuff with x1
        | other -> failwith "Invalid type"

@baronfel
Copy link
Contributor

baronfel commented Sep 8, 2024

Another example of teams adding APIs that causes a break in F# code: microsoft/testfx#2381

@vzarytovskii
Copy link

Another example of teams adding APIs that causes a break in F# code: microsoft/testfx#2381

Yeah, it's everywhere unfortunately. Faced it in bcl when upgraded sdk. We will eventually do it, but it will have to be approached very carefully, especially around considerations for "better" overloads (such as span based), since we are unlikely to change those once we implement them. It will need a very thorough RFC, I might write it once we done with November release.

@En3Tho
Copy link

En3Tho commented Sep 9, 2024

@vzarytovskii Would be amazing to have. This will reduce a huge number of hacky workarounds (like using module import order) and simplify using generics.

@vzarytovskii
Copy link

vzarytovskii commented Sep 16, 2024

For future me:

  1. Test with (and take into account) byreflikes (span, ros, etc) into account and prefer the most concrete one as suggested.
  2. Add a diagnostic message mentioning that other overloads are available and that we selected the most "concrete one".
  3. Take resolution priority attribute into account
  4. Make sure we do correct thing (correct things is yet to be defined) with optional parameters (like member _.Foo(s: string) and member _.Foo(s:string, ?o: string), etc).
  5. params should also resolve properly
  6. Make sure extension methods priorities stay the same even with tiebreakers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants