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
Use an Attribute to specify overload resolution priority #821
Comments
Another inconvenient is that the overload would be resolved differently in F# and in C#. Obviously it's not a problem in your use case of TaskBuilder, but still something to consider in general. |
I remember trying to consider what it means to have an attribute as evocated in this suggestion, thinking of solutions like absolute index ranking but also other more "out of the box" aproaches. It feels to me the suggestion is not precise enough (defining exact solution that is proposed) and that, in essence, it is working around flaws in current overload resolution because this mecanism is the only approach F# currently offers for compile time resolved polymorphism. Enabling such extension of overload resolution is not really attacking more general and simpler issues with those (for example, making code like in dotnet/fsharp#2503 (comment) compile without needing more anotations or being more counter intuitive than what C# offers, to add to @Tarmil armil comment) but providing an escape hatch to enable slightly better story at designing APIs like F#+, whose sole option right now is using overload resolution and some internal knowledge of the compiler implementation quirks of it. Designing APIs like F#+ provides currently mandates to rely on method overloading mecanism and really are a work around to the lack of extensible and compile time resolved polymorphism constructs in dotnet languages. F# enables working around in limited maner with SRTP and overloads, accidentally, if I understand what Don Syme has expressed about the reasons SRTP exist in the language design and the F#+ implementation idioms. The problem for designing those APIs (which do serve purpose, and can't be sumarized as just pointless abstractions) is not all with overload resolution being a bit of an issue as it is right now, but more with lack of other, more appropriate constructs to encode those approach to compile time resolved polymorphism. It is given that it will be long before the things like traits (Rust parlance), type classes (Haskell parlance) or concepts (C++ from the future parlance) can land in the language (and/or the CLR), and adding what feels like unatural constructs to overload resolution sounds like more bagage for F# to carry in the meantime. Bagage which should be removed in an ideal F# from the future where alternative(s) mecanism(s) may actually come in picture and enable APIs like F#+ to be encoded idiomatically, not so counterintuively as the languages forces right now. I'm more keen on efforts to make F# be on par with C# where it currently fails to pick an overload, than exploring exotic solutions that remain strictly on "overload resolution being quirkier down the road" angle. The aim of deep compiler work on overload resolution should be to be able to simplify the implementation, enabling reasoning about it / optimizing it for general cases (consuming regular OO BCL APIs, and devising similar APIs consumed idiomatically in OO languages) to more people than those currently knowing how it ends up working. (sorry that was really long, hope some of my points can sprout discussions on the suggestion or the matter of overload definition/resolution ethics...) |
Do you think they resolve the same right now :) I think, an attribute as last resort, knowing that it's an F# only construct as there are many others which will be ignored from other languages, is not that bad. I still remember the old Anyway, if someone have a better suggestion to solve this problem I would like to hear it. |
I just came out with a better option. Currently the compiler can look for overloads defined in base types:
But by doing a trait call involving It would be nice to strictly give priority to methods in This is much better than having a marker type passed as parameter (like the ones in the taskbuilder), which "pollutes" the signature of the member for the sole purpose of resolving ambiguity. Another possibility is to use intrinsic extensions, it currently has an effect in certain cases, but not always. |
@gusty, isn't that what's currently happening? If I define a method in Or is the latter precisely what you don't want to happen? If so, changing it would potentially be a backwards compatibility issue. |
@abelbraaksma see answers below:
True
True only for direct overload calls, but false for trait-calls
True, and I would say I'm fine with this
All changes are more or less potentially backwards compatibility issues. @dsyme this is something it might interest you, at least for your SRTP tests |
Thanks for the detailed answer and explanation. It's now much clearer to me what you're proposal covers. |
As a side note, in general direct overload call doesn't match trait call overload. See this small repro:
|
Could we instead update the compiler to have a deterministic rule to break ambiguity? My thought is to have it pick the last method that was declared which matches. This is a silly example but it illustrates my point: type IntBuilder () =
member this.Yield (i:int) =
i
member this.For(source:seq<'a>, body:'a -> seq<'b * int>) =
source
|> Seq.collect (fun x -> body x |> Seq.map (fun (idx, i) -> (x, idx), i))
member this.For(source:seq<'a>, body:'a -> int) =
source |> Seq.map (fun x -> x, body x)
member inline this.Run(source:seq<'a * int>) =
source
|> Seq.map (fun (x, d) -> x, d)
member inline this.Run(source:seq<('a * 'b) * int>) =
source
|> Seq.map (fun ((x, y), d) -> (x, y), d)
member inline this.Run(source:seq<('a * ('b * 'c)) * int>) =
source
|> Seq.map (fun ((x, (y, z)), d) -> (x, y, z), d)
member inline this.Run(source:seq<('a * ('b * ('c * 'd))) * int>) =
source
|> Seq.map (fun ((x, (y, (z, a))), d) -> (x, y, z, a), d)
let intBuilder = IntBuilder ()
let c = intBuilder {
for i in 1..2 do
for j in 1..2 do
for k in 1..2 do
for l in 1..2 ->
i + j + k + l
} The line where I try to create Clearly this wouldn't break any code bases because this code cannot be written right now. It would enable use cases like this one. The behavior of "Last Method Declared Wins" follows the conventions of the F# compiler so it is easy to figure out what the compiler is going to do. |
I propose we add a way to use an Attribute to specify overload resolution priority
The existing way of approaching this problem in F# is adding a "marker" dummy parameter with a concrete class which inherit from other objects which will be used to mark less priority, since the overload resolution prefers the type which is closer in hierarchy.
This will be very valuable when using member constraints, projects like F#+ or the Task Builder, including the upcoming version baked into the compiler, make use of this workaround due to lack of a formal mechanism.
Pros and Cons
The advantages of making this adjustment to F# are:
The disadvantages of making this adjustment to F# are:
Extra information
Estimated cost: S:
Related suggestions: #819 #820
Affidavit (please submit!)
Please tick this by placing a cross in the box:
Please tick all that apply:
The text was updated successfully, but these errors were encountered: