Simplify implementation of interface hierarchies with equally named abstract slots
I propose we do something about the situation described here: https://github.com/dotnet/runtime/pull/116497 , which was triggered by a desired change in the BCL to make mutable generic ICollection interfaces implement IReadOnlyCollection types. This change now has a PR suggesting its revert, until F# makes it easier to consumer and/or to workaround.
Causing:
type C<'T> =
interface ICollection<'T> with
// members omitted for clarity
member s.Count = s.Count
/* --------------^^^^^ error FS0361: The override 'get_Count: unit -> int' implements more than one abstract slot
, e.g. 'ICollection.get_Count() : int' and 'IReadOnlyCollection.get_Count() : int' */
The existing way of approaching this problem in F# is to know to explicitly declare implemented interfaces in case of slot conflicts.
Pros and Cons
The advantages of making this adjustment to F# are : Decrease chances of a BCL/C# change causing a source-break for F# programs.
The disadvantages of making this adjustment to F# are : It is a change.
Extra information
Estimated cost (XS, S, M, L, XL, XXL):
Related suggestions: (put links to related suggestions here)
Affidavit (please submit!)
Please tick these items 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] This is a language change and not purely a tooling change (e.g. compiler bug, editor support, warning/error messages, new warning, non-breaking optimisation) belonging to the compiler and tooling repository
- [ 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
- [ x] I have searched both open and closed suggestions on this site and believe this is not a duplicate
Please tick all that apply:
- [x ] 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 :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.
Worth noting that the following scenarios work fine...
Scenario 1: Single Interface defines both defaulted and non-defaulted members
public interface IA
{
int A();
int B() => A();
}
type C =
member _.A() : int = 0
interface IA with
member s.A() = s.A()
Scenarios 2: Derived interfaces defines a new defaulted member
public interface IA
{
int A();
}
public interface IB : IA
{
int B() => A();
}
type C =
member _.A() : int = 0
interface IB with
member s.A() = s.A()
What isn't working is the following...
Scenario 3: Derived interfaces defines a defaulted implementation for a base interface member
public interface IA
{
int A();
}
public interface IB : IA
{
new int A();
int IA.A() => A();
}
type C =
member _.A() : int = 0
interface IB with
// The override 'A: unit -> int' implements more than one abstract slot, e.g. 'IB.A() : int' and 'IA.A() : int'
member s.A() = s.A()
Instead it is currently required that you explicitly declare that IA is also implemented:
type C =
member _.A() : int = 0
interface IB with
member s.A() = s.A()
interface IA
The general issue here is that it makes the scenario where you opt to make IB start implementing IA via a DIM a source breaking change for F#.
The best outcome would be if F# did not require you to explicitly declare interface IA because a DIM exists for IA.A() already, so it would not be source breaking and it would seem to be generally consistent with how DIMs are intended to work and are likely getting used in the real world.
Some warning might be okay to let users know that a new interface is part of the hierarchy, but other languages like C++/CLI and C# aren't doing this themselves. They only error if there are concrete diamond problems encountered instead.
Diamond problems caused by DIMs still have to be detected, indeed. This situation could be lifted from the warning. I kind of understand the original defensiveness for arbitrary abstract slot problems (outside of the DIM context) - there is no guarantee that IA.A() and IB.A() should be semantically the same operations. It might have been just a coincidence that they share the same name.
However, with DIMs and the interfaces being in the same hierarchy, this carefulness can be lifted.
type C = member _.A() : int = 0 interface IB with // The override 'A: unit -> int' implements more than one abstract slot, e.g. 'IB.A() : int' and 'IA.A() : int' member s.A() = s.A()
I may have misunderstood but why does F# need to target both IB.A() and IA.A() here? Since IA.A() is already implemented by a DIM, I would expect F# to leave that method alone.
Specifically, I would expect this:
public interface IB : IA
{
new int A();
int IA.A() => 0;
}
type C =
interface IB with
member s.A() = 1
To implement IB.A() but still return 0 from IA.A().
(By the way it is great to see this addressed ‒ keep rocking!)
I may have misunderstood but why does F# need to target both IB.A() and IA.A() here?
Binding to both is exactly how C# works. By default it binds to all applicable interface signatures. You need to be explicit if you want a different behavior.
https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEUCuA7AHwAEAmARgFgAoagYTIAIxGBeBvGAdwfoAoBKANzUiZAJy8ARPUlCR43ooCSAQX7N+AOhUC5VUROUAhdWS07+egwOE0qtEk0dsO3BzfkTpJWbevK1MBJzXT8FY3Vg7VDPDzsiAGYGAEs8DBgoADMAQzAYBlVqAG9qBjKUtIYLWwBfahEk1PSs3PylIwYQApVi0vLXCowquL6ypu7o/gYWAD4GMlr6/STSHkYu9t6qcsGCo0npuYAGRfiVxwdOva2dxN2LQ4YT6jqqIA===
Yes, but F# uses interface IB with so it is already "semi-explicit" in terms of which interface is intended. Of course this still allows binding to inherited interface members, but to me it seems F# has the opportunity to acknowledge the DIM shadowing and pick only IB.A(), unlike C# (which requires precision for explicit interface implementation). That behaviour simply negates the reason for any such error in the first place, because you don't even need to implement IA.A().
That behaviour simply negates the reason for any such error in the first place, because you don't even need to implement
IA.A().
Currently the error appears whenever there are 2 or more conflicting dispatch slots to be implemented. The typechecker could detect via DefaultInterfaceImplementationSlot that all but 1 dispatch slots are covered with a DIM - in that case allow that and don't report.
Otherwise keep existing behavior. ?
For classes this already works, but interface types are excluded from this logic:
https://github.com/dotnet/fsharp/blob/6596de5d37eaac12aca400fabd08500f169f1fdc/src/Compiler/Checking/MethodOverrides.fs#L632-L639
Any chance to make it anytime soon? I would like to get mutable generic collection interfaces to implement readonly ones in an early phase (ideally before .NET 11 previews being rolled out) so that we can have more time to evaluate the impact (and fix them if there're any), and make it in .NET 11 finally.
Now that .Net 10 is nearly out, is there any chance for this to be picked up again? It would be very good to have this ready, so the linked C# issue can be worked on again for .Net 11.
This suggestion is approved in principle - once designed (via a RFC) and implemented (by anyone), it can be reviewed and merged in dotnet/fsharp.
I am happy to guide anyone interested trough all parts of the process 👍.