Description
I propose we add support for consumption and production of methods on interfaces with concrete implementations to enable the following:
- Interoperation with C# and .NET when it is implemented
- Help API authors version interfaces
- Aid interoperation with Java and iOS for Xamarin programming scenarios
- Act as a basis for trait-like programming, potentially using this as a vehicle for further abstractions
The existing way of approaching this problem in F# is to either:
- Use abstract classes; though realistically, this is not done much because it forces implementation inheritance.
- Not communicate via interfaces.
Additionally, it is important to call out that this requires a runtime change for .NET. The change for the runtime is currently being driven and coordinated by the C# and .NET runtime teams.
Description
The syntax for interfaces would be extended to permit the following:
- A concrete body for a member (i.e., a default implementation)
- Explicit access modifiers
- Overriding members
- Static members
❗️ The following syntax and concepts are a starting point, and absolutely need discussion. ❗️
Concrete member bodies
To begin, there needs to be a way to specify a concrete body for a member in an interface:
type IA =
default M() = printfn "IA.M()"
A class that implements this interface need not implement its concrete method:
type C() =
interface IA
let ia: IA = C()
ia.M() // Prints "IA.M()"
The same would apply for object expressions:
type IA =
default M() = printfn "IA.M()"
type IB =
inherit IA
abstract N: unit -> unit
let x =
{ new IB with
member __.N() = () }
x.M() // Prints "IA.M()"
Note that a class does not inherit members from the interface, so the following code would be illegal:
C().M() // ERROR, 'C' does not contain member 'M'
Member modifiers
The default modifier for interface members is public
. That is, the following member is public:
type IA =
default M() = printfn "IA.M()"
The following access modifiers could be added: public
, internal
, and private
:
type IModifiers =
abstract internal Doop: unit
abstract private Hoopty: unit
This is keeping in line with access modifiers being added in C#.
Overriding members
Interfaces could override default members from other interfaces:
type IA
default M() = printfn "IA.M()"
type IB
inherit IA
override IA.M() = printfn "IB.M()" // explicitly named
type IC
inherit IA
override M() = printfn "IA.M()" // implicitly named
Open question: Should explicit naming like this even be a thing?
Overrides are useful to provide more useful "versions" of methods. For example, a new First()
method on IEnumerable
may have a much more efficient implementation on the interface IList
.
Note that no overriding would occur unless specified with the keyword override
.
Reabstraction
A default member in an interface could also be overridden to be made abstract again:
type IA =
default M() = printfn "IA.M()"
type IB =
inherit IA
override abstract M: unit -> unit
type C() =
inherit IB // Error: 'C' does not implement member 'IA.M'
If permitted, the abstract
keyword should be required. This is currently also an open issue for C#. We should only implement this if C# does.
Most specific override
I would imagine we do the same as C# here:
We require that every interface and class have a most specific override for every virtual member among the overrides appearing in the type or its direct and indirect interfaces. The most specific override is a unique override that is more specific than every other override. If there is no override, the member itself is considered the most specific override.
For example:
type IA
default M() = printfn "IA.M()"
type IB
inherit IA
override IA.M() = printfn "IB.M()"
type IC
inherit IA
override IA.M() = printfn "IC.M()"
interface ID
inherit IB
inherit IC // ERROR: no most specific override for 'IA.M'
type C()
inherit IB
inherit IC // ERROR: no most specific override for 'IA.M'
type D() // OK
inherit IA
inherit IB
abstract M: unit -> unit
This rule ensures that ambiguities from diamond inheritance are resolved by the programmer.
static
and private
members
Because interfaces could have concrete default implementations, common code could be abstracted into static
and private
members. There are some open issues for C# here today.
This could, in effect, turn interfaces into something in between what we use today and abstract classes. It's a bit iffy to me. I wouldn't want something like let
-bound values in them, either.
Base interface invocation
Code in a type that derives from an interface with a default method can explicitly invoke that interface's "base" implementation:
type IA
default M() = printfn "IA.M()"
type IB
inherit IA
override M() = printfn "IB.M()"
type IC
inherit IA
override M() = printfn "IC.M()"
type ID
inherit IB
inherit IC
override IA.M() = IB.base.M()
Note that the syntax is Type.base.Member
. This is, in keeping with the theme of this suggestion, in line with the C# spec for the feature.
Additionally, the C# team has verified that these changes would not affect existing programs. I would expect anything we do for F# also not affect existing programs.
Pros and Cons
The advantages of making this adjustment to F# are:
- Ability to version interfaces over time
- Ability to begin writing trait-like programs
- Nicer interop with Android and iOS for Xamarin
- Interoperation with the direction C# and the CLR would take
- A possible vehicle for higher abstractions
The disadvantages of making this adjustment to F# are
- Changes to a fundamental type (interfaces) put cognitive load on people that is arguably unnecessary
- This feels like it solves less of a problem for F# than it does for C#
- It's considerable work to implement, get right, and document
Extra information
Estimated cost (XS, S, M, L, XL, XXL): XL
Related information:
- C# proposal champion issue: Champion "default interface methods" (16.3, Core 3) dotnet/csharplang#52
- C# spec for default interface methods
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