fslang-suggestions icon indicating copy to clipboard operation
fslang-suggestions copied to clipboard

"Most concrete" tiebreaker for generic overloads

Open NinoFloris opened this issue 5 years ago • 8 comments

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:

  • https://github.com/rspeele/TaskBuilder.fs/blob/master/TaskBuilder.fs#L339
  • https://github.com/demystifyfp/FsToolkit.ErrorHandling/blob/master/src/FsToolkit.ErrorHandling/AsyncResultCE.fs#L96
  • Even FSharp Core itself with the query {} CE https://github.com/dotnet/fsharp/blob/fead0aac540485683f694524eadad79983ec28d9/src/fsharp/FSharp.Core/Query.fs#L210-L221

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:

  • [x] This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • [x] I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • [x] 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
  • [x] 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 :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.

NinoFloris avatar Aug 25 '20 13:08 NinoFloris

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.

cmeeren avatar Aug 28 '20 22:08 cmeeren

Yes this seems reasonable.

dsyme avatar Jan 12 '21 14:01 dsyme

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.

En3Tho avatar Feb 17 '22 08:02 En3Tho