-
Notifications
You must be signed in to change notification settings - Fork 21
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
Prefer extension methods to intrinsic properties when arguments are provided #1039
Comments
@laenas I think the proposal you want it "prefer extension methods to intrinsic properties when arguments are given" Could you provide a code sample too please? |
@dsyme type Bar() =
member val B : (int -> unit) = (fun i -> printfn "%i" i) with get,set
[<Extension>]
type BarExt =
[<Extension>]
static member B (b:Bar, _:int) = "Nothing yet"
let b = Bar()
b.B(1) Works as I'd expect, for example - just ignoring the extension method and printing |
Yes, this would be a breaking change for this case. I doubt it occurs in practice (why would anyone have defined the unusable extension method?), but we could code a specific rule to prefer a function-type intrinsic property just in case. |
But that's just it - I'm not really advocating for changing the preference here - so much as just enabling us to properly consume non-conflicting extension methods (that overload a name). If they happen to get hidden by intrinsics in these edge cases, well, maybe some analysis and a compiler warning - but that's at least solvable. The fact that we can't even consume them as intended is problematic, and seems to be owing to that very sharp divide made of deciding immediately whether a given identifier belongs to a Property or a Method on a type. We check properties first - which is on the surface fine - but in the case that the typecheck fails, rather than aborting out it seems to make sense that we should fallback and at least see if there is something else available for that identifier that would fit. |
Both C# and F# make this distinction early - we do not allow overloading involving both properties and methods, and indeed non-indexer properties can't be involved in overloading at all. I don't think we really want to modify this restriction to allow this in the door. |
I must be conflating and misusing some terminology somewhere here 😅 #r "nuget: Spectre.Console"
using Spectre.Console;
var table = new Table();
table.Width = 100;
Console.WriteLine(table.Width); //100
table.Width(200);
Console.WriteLine(table.Width); //200 (ref: https://github.com/spectreconsole/spectre.console/blob/865552c3f2046282519b85e543ed516142f7785a/src/Spectre.Console/Widgets/Table/Table.cs#L55 & https://github.com/spectreconsole/spectre.console/blob/865552c3f2046282519b85e543ed516142f7785a/src/Spectre.Console/Extensions/TableExtensions.cs#L152 ) In F#, compiler error because it doesn't 'see' that you're actually attempting to access the extension method. #r "nuget: Spectre.Console"
open Spectre.Console
let table = Table()
table.Width <- 100
printfn "%A" table.Width
table.Width(200) //FS0003 - This value is not a function...
printfn "%A" table.Width If you try to define a property and method of the same name intrinsic to a class, you do get an error in C# - in alignment with the error for doing the same thing in F#: you get divergent behavior once extension methods are taken into account. C# will absolutely tolerate this sort of overloading, and I use Spectre as an example because it's something I've seen come up in community discussions a few times now since their documentation uses this feature and causes issues for folks trying to interop from F#. |
I believe the difference between C# and F# is that C# see F# looks at |
Yep, that's a reasonable description of what's going on in the typechecker when this throw the error. And we do 'know' towards the top of this process that the next thing 'in line' is going to be an App, so it just feels like we should probably be able to resolve this in a similar manner without actually breaking any present behavior - albeit with some possible shaking up of the typechecker call chain. |
When I encountered this some time ago I wasn't able to figure out a workaround without asking in Stackoverflow: https://stackoverflow.com/questions/66368514/how-to-call-extension-method-when-there-is-a-property-with-same-name |
This feature is supposed to create an alternative to: //Every C# dev knows this pain:
var f = new Foo();
f.X = 1;
f.Y = 2; However in both C# and F# there is better syntax for this. var f = new Foo(X=1, Y=2) That said, the feature may be required for C# compat as in @hvester 's post. |
public void Foo(Bar b)
{
b.PropA = 1;
b.PropB = 2;
}
//becomes
public void Foo(Bar b)
{
b.A(1).B(2);
} Constructor-equivalence isn't the point, it's the ability to effectively 'extend' properties with fluent setters. |
From a practical perspective - Spectre.Console is pretty much the terminal library in .Net, and if we can't use it (or make it easy for newcomers to use it) that looks very bad for F#. I don't think this is something we can just ignore. |
It's unfortunate. C# only supports extension methods. It always prefers methods on the original type. But if there's a property with that name, it doesn't care and prefers the extension method anyway. TBH no one should be designing APIs where an extension method conflicts with an intrinsic property on the existing type. I'm not at all sure what the resolution of this should be. In the case where arguments are give, I guess we could prefer the extension method over intrinsic properties. But even that would be a breaking change in some corner cases (the property would have to have F# function type - which is not going to happen for .NET-defined types) Note that as a workaround you can call the extension method using the explicit call FooExt.X(foo, 4) or use this technique to define a new extension method giving a different name: [<Extension>]
static member X2 (f: Foo, i: int) = FooExt.X(f, i) |
Any info on this? I have an increasing number of C# libraries where this pattern occurs, and the resulting cooperation problems between F# and C# are becoming very annoying. :( |
Not the same at all, but slightly related issue: dotnet/fsharp#3692 |
@maciej-izak could you show some examples of C# and client F# code? If it is about setting properties, you could make a single extension method in F# for the type, and pass it the property assignment: open System.Runtime.CompilerServices
type Foo() =
member val X : int = 0 with get,set
[<Extension>]
type FooExt =
[<Extension>]
static member Set (f: Foo) = f
let f =
Foo((* somehow the user doesn't want to set properties here*))
.Set(X=1)
printfn $"%i{f.X}" @maciej-izak, assuming @dsyme would concede to the pattern (despite it is not nice one, why not use the @gusty, do you know if it is possible to make a function that is like |
Yes, it's possible. I just tried modifying the generic |
@smoothdeveloper sure here you can check the problem, fsharp can't consume extension methods with the same name as properties, there was needed new package and special fix to solve F# limitations (!), here is detailed description : Whole NXUI is one big example :). I found this problem also few times for other libs like dnLib and cecil |
I did some digging/note to self: https://github.com/dotnet/fsharp/blob/3df945aa3a85b2562864b393aabad39506a13f72/src/Compiler/Checking/NameResolution.fs#L2609 is the place where property is looked up first a bit after is the check for methods / extension methods:
I'll ponder some more on a good way to make the resolution more explicit, it currently relies on "head of the list" being picked when there are several things, I think wrapping both |
Another source of ambiguity is indexed properties, they allow notation such as "abc".Chars 0
type Foo() =
member _.X
with get (x,y) = x,y
and set (x,y) value = ()
let f = Foo()
f.X (1,"")
f.X (1,"") <- 3 |
Can this be closed based on dotnet/fsharp#16032 ? cc @vzarytovskii |
I'd say so, yeah, thanks for noticing |
I propose we add support for C#-style extension methods which share a name but not a type with a property on a type. Some libraries (for example, Spectre.Console) use this behavior to enable extensions methods to create fluent APIs for the setting of values on their types.
Consider the following abstracted type definition:
What we should be able to do is dot through the assignment:
While the benefits are not immediately apparent in an F#-centric approach, consider from an interop perspective (both consuming and being consumed from C#) and in the case of multiple properties:
The existing way of approaching this problem in F# is:
To achieve the same effect in F#, you need to create an entirely new set of wrapper functions - either type extensions that have names which don't conflict with existing properties, or a module of helper functions that can be pipelined instead of called fluently.
Pros and Cons
The advantages of making this adjustment to F# are a greater degree of interop with behaviors made possible in the CLR - and which are already being seen in the wild in C# projects and libraries which F# consumers have a use for, as well as enabling F# to expose similar behaviors to C# consumers who don't have the ability to pipeline in an F#-native way.
The disadvantages of making this adjustment to F# are the additional complexity of overload resolution, both from a compiler standpoint and from the possibility of introducing subtle confusion into compiler error messages when a user accidentally uses the property's getter instead of invoking the method of the same name, or visa versa.
Extra information
Estimated cost (XS, S, M, L, XL, XXL): M? L?
Related suggestions: (put links to related suggestions here)
Affidavit (please submit!)
Please tick this 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 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.
Additional References
The text was updated successfully, but these errors were encountered: