dotnet / csharplang

The official repo for the design of the C# programming language
11.39k stars 1.02k forks source link

[Proposal]: Static abstract members in interfaces #4436

Open MadsTorgersen opened 3 years ago

MadsTorgersen commented 3 years ago

Static abstract members in interfaces

Speclet: https://github.com/dotnet/csharplang/blob/main/proposals/csharp-11.0/static-abstracts-in-interfaces.md

Summary

An interface is allowed to specify abstract static members that implementing classes and structs are then required to provide an explicit or implicit implementation of. The members can be accessed off of type parameters that are constrained by the interface.

Motivation

There is currently no way to abstract over static members and write generalized code that applies across types that define those static members. This is particularly problematic for member kinds that only exist in a static form, notably operators.

This feature allows generic algorithms over numeric types, represented by interface constraints that specify the presence of given operators. The algorithms can therefore be expressed in terms of such operators:

// Interface specifies static properties and operators
interface IAddable<T> where T : IAddable<T>
{
    static abstract T Zero { get; }
    static abstract T operator +(T t1, T t2);
}

// Classes and structs (including built-ins) can implement interface
struct Int32 : …, IAddable<Int32>
{
    static Int32 I.operator +(Int32 x, Int32 y) => x + y; // Explicit
    public static int Zero => 0;                          // Implicit
}

// Generic algorithms can use static members on T
public static T AddAll<T>(T[] ts) where T : IAddable<T>
{
    T result = T.Zero;                   // Call static operator
    foreach (T t in ts) { result += t; } // Use `+`
    return result;
}

// Generic method can be applied to built-in and user-defined types
int sixtyThree = AddAll(new [] { 1, 2, 4, 8, 16, 32 });

Syntax

Interface members

The feature would allow static interface members to be declared virtual.

Today's rules

Today, instance members in interfaces are implicitly abstract (or virtual if they have a default implementation), but can optionally have an abstract (or virtual) modifier. Non-virtual instance members must be explicitly marked as sealed.

Static interface members today are implicitly non-virtual, and do not allow abstract, virtual or sealed modifiers.

Proposal

Abstract virtual members

Static interface members other than fields are allowed to also have the abstract modifier. Abstract static members are not allowed to have a body (or in the case of properties, the accessors are not allowed to have a body).

interface I<T> where T : I<T>
{
    static abstract void M();
    static abstract T P { get; set; }
    static abstract event Action E;
    static abstract T operator +(T l, T r);
}

Open question: Operators == and != as well as the implicit and explicit conversion operators are disallowed in interfaces today. Should they be allowed?

Explicitly non-virtual static members

Todau's non-virtual static methods are allowed to optionally have the sealed modifier for symmetry with non-virtual instance members.

interface I0
{
    static sealed void M() => Console.WriteLine("Default behavior");

    static sealed int f = 0;

    static sealed int P1 { get; set; }
    static sealed int P2 { get => f; set => f = value; }

    static sealed event Action E1;
    static sealed event Action E2 { add => E1 += value; remove => E1 -= value; }

    static sealed I0 operator +(I0 l, I0 r) => l;
}

Implementation of interface members

Today's rules

Classes and structs can implement abstract instance members of interfaces either implicitly or explicitly. An implicitly implemented interface member is a normal (virtual or non-virtual) member declaration of the class or struct that just "happens" to also implement the interface member. The member can even be inherited from a base class and thus not even be present in the class declaration.

An explicitly implemented interface member uses a qualified name to identify the interface member in question. The implementation is not directly accessible as a member on the class or struct, but only through the interface.

Proposal

No new syntax is needed in classes and structs to facilitate implicit implementation of static abstract interface members. Existing static member declarations serve that purpose.

Explicit implementations of static abstract interface members use a qualified name along with the static modifier.

class C : I<C>
{
    static void I.M() => Console.WriteLine("Implementation");
    static C I.P { get; set; }
    static event Action I.E;
    static C I.operator +(C l, C r) => r;
}

Open question: Should the qualifying I. go before the operator keyword or the operator symbol + itself? I've chosen the former here. The latter may clash if we choose to allow conversion operators.

Semantics

Operator restrictions

Today all unary and binary operator declarations have some requirement involving at least one of their operands to be of type T or T?, where T is the instance type of the enclosing type.

These requirements need to be relaxed so that a restricted operand is allowed to be of a type parameter that is constrained to T.

Open question: Should we relax this further so that the restricted operand can be of any type that derives from, or has one of some set of implicit conversions to T?

Implementing static abstract members

The rules for when a static member declaration in a class or struct is considered to implement a static abstract interface member, and for what requirements apply when it does, are the same as for instance members.

TBD: There may be additional or different rules necessary here that we haven't yet thought of.

Interface constraints with static abstract members

Today, when an interface I is used as a generic constraint, any type T with an implicit reference or boxing conversion to I is considered to satisfy that constraint.

When I has static abstract members this needs to be further restricted so that T cannot itself be an interface.

For instance:

// I and C as above
void M<T>() where T : I<T> { ... }
M<C>();  // Allowed: C is not an interface
M<I<C>>(); // Disallowed: I is an interface

Accessing static abstract interface members

A static abstract interface member M may be accessed on a type parameter T using the expression T.M when T is constrained by an interface I and M is an accessible static abstract member of I.

T M<T>() where T : I<T>
{
    T.M();
    T t = T.P;
    T.E += () => { };
    return t1 + T.P;
}

At runtime, the actual member implementation used is the one that exists on the actual type provided as a type argument.

C c = M<C>(); // The static members of C get called

Drawbacks

Alternatives

Structural constraints

An alternative approach would be to have "structural constraints" directly and explicitly requiring the presence of specific operators on a type parameter. The drawbacks of that are:

Default implementations

An additional feature to this proposal is to allow static virtual members in interfaces to have default implementations, just as instance virtual members do. We're investigating this, but the semantics get very complicated: default implementations will want to call other static virtual members, but what syntax, semantics and implementation strategies should we use to ensure that those calls can in turn be virtual?

This seems like a further improvement that can be done independently later, if the need and the solutions arise.

Virtual static members in classes

Another additional feature would be to allow static members to be abstract and virtual in classes as well. This runs into similar complicating factors as the default implementations, and again seems like it can be saved for later, if and when the need and the design insights occur.

Unresolved questions

Called out above, but here's a list:

Design meetings

gulbanana commented 3 years ago

there will be limits to C#'s evolution, but we're not there yet. static abstract members are a useful tool for application code (.Parse etc) that don't require a BCL retrofit

leandromoh commented 3 years ago

there will be limits to C#'s evolution

@gulbanana what do you mean? can you give some examples, please?

gulbanana commented 3 years ago

what i mean is that some potential features would be too significant to retrofit. C# will never have a rust-style borrow checker, for example; there is a legacy of decades of code which assumes garbage collection and safe circular references.

if someone invented a brilliant new language feature tomorrow, it’s possible that backward compatibility would prevent it from coming to .NET, or render it useless in context. this is all pretty hypothetical, though - C# is not yet burdened enough to fall behind its competitors in industry. if anything it’s well ahead of the major alternatives (go, kotlin, etc)

otac0n commented 3 years ago

I would expect this to extend to TimeSpan, DateTime, DateOnly, TimeOnly, etc.

My requirements are that I have can include a library which allows me to multiply any TimeSpan by any <scalar> by only adding a file-or-namespace-scoped declaration.

theunrepentantgeek commented 3 years ago

@otac0n, how about using System; ?

public void M() {
    var oneDay = TimeSpan.FromDays(1);
    var oneWeek = oneDay * 7;
}

Your requirement is already valid C# code, and has been as far back as C# 1.1 AFAIK. Verified on SharpLab.

Joe4evr commented 3 years ago

@theunrepentantgeek Just to verify, that operator did not exist in .NET 1.1; it was only introduced in Core 2.0/Net Standard 2.1.

theunrepentantgeek commented 3 years ago

Just to verify, that operator did not exist in .NET 1.1 ...

Thanks! I thought I remembered using the operator in a .Net 1.1 project, I must be mistaken. Appreciate the correction.

Dweeberly commented 3 years ago

Open question: Operators == and != as well as the implicit and explicit conversion operators are disallowed in interfaces today. Should they be allowed?

Yes

"Todau's non-virtual static methods"

Spelling, trivial

Open question: Should the qualifying I. go before the operator keyword or the operator symbol + itself? I've chosen the former here. The latter may clash if we choose to allow conversion operators. Before the operator keyword.

Semantically, I see "operator+" as the method name, rather than "+". Syntactically, I.+ seems more "proper", but the operator keyword must be paired with a token, so again the pair act as a unit, supporting the I.operation form.

Open question: Should we relax this further so that the restricted operand can be of any type that derives from, or has one of some set of implicit conversions to T?

Yes, I see it as something of a mindset. I'd rather define what something can do/be, rather than constrained by what it can't. At least in this context.

Interface constraints with static abstract members

Could this section be expanded on? Isn't M<I> using any object that implements I as it's generic?

OronDF343 commented 3 years ago

dotnet/runtime#52173 implements this proposal in the runtime, correct? This DevBlogs article says that it will available in C#10. What exactly is the scope that is currently planned to be shipped, with regards to abstract/virtual, class/interface, DIM?

About generic constraints, the proposal says that if a generic parameter T has a constraint such that it must inherit from an interface that has static abstract members, then it is required that T must be a non-abstract class. In my opinion, this will cause confusion if an abstract static method is added to an existing interface. In such a case, existing code with generic parameters may not compile due to the introduction of this "hidden" constraint.

A possible solution would be to add a constraint keyword similar to new() that requires T to be a non-abstract class, but does not require specifically a parameterless constructor (I think that new without parenthesis makes the most sense as it implies "the ability of an instance of concrete type T to exist" which is the same as "T is a non-abstract type"). However, this solution has a few issues (may be redundant in many cases, adds complexity, not very useful to most people).

In my opinion, there isn't a perfect solution. For now, one of these may help as an alternative:

  1. Do not introduce the "hidden" constraint. Instead, produce a compiler warning/error when calling an abstract interface member in a context where an implementation may not exist. If an attempt is made to call a static method that doesn't have an implementation at runtime, throw an exception.
  2. Option 1 + Do not allow abstract static methods in classes, only in interfaces. The developer may use the class constraint to avoid the compiler error, since classes are guaranteed to provide an implementation.
  3. Require static interface methods to have a default implementation. This means that only virtual static members (DIM for interfaces) will exist, not abstract ones.
  4. Option 1 + Introduction of new constraint to avoid the compiler error. This option is the most complete, but is also more verbose and requires the most work to implement.

In my opinion, option 4 is best, while option 2 is a viable alternative requiring less new syntax. Option 3 is only useful as a temporary measure, for example if option 4 is planned for a future version, but it does require static DIM support. Option 1 on its own is not good enough, and another solution should be devised. (The DevBlogs article linked above calls the feature "static virtual interface methods", which implies option 2 or 3)

Anyway, this was just my take. I'd like to hear some more thoughts about this scenario where T may or may not have an implementation for a static abstract member.

Joe4evr commented 3 years ago

What exactly is the scope that is currently planned to be shipped, with regards to abstract/virtual, class/interface, DIM?

You can see this write-up for that.

About generic constraints, the proposal says that if a generic parameter T has a constraint such that it must inherit from an interface that has static abstract members, then it is required that T must be a non-abstract class.

Really? Where does it say that?

In my opinion, this will cause confusion if an abstract static method is added to an existing interface. In such a case, existing code with generic parameters may not compile due to the introduction of this "hidden" constraint.

Adding a new member to an existing interface is already a breaking change, why is this any different?

OronDF343 commented 3 years ago

About generic constraints, the proposal says that if a generic parameter T has a constraint such that it must inherit from an interface that has static abstract members, then it is required that T must be a non-abstract class.

Really? Where does it say that?

Here:

Interface constraints with static abstract members

Today, when an interface I is used as a generic constraint, any type T with an implicit reference or boxing conversion to I is considered to satisfy that constraint.

When I has static abstract members this needs to be further restricted so that T cannot itself be an interface.

In my opinion, this will cause confusion if an abstract static method is added to an existing interface. In such a case, existing code with generic parameters may not compile due to the introduction of this "hidden" constraint.

Adding a new member to an existing interface is already a breaking change, why is this any different?

Admittedly, I was thinking of a non-trivial situation. Example:

// In assembly A1
public interface IA
{
    int Foo();
    string Bar();
}
public abstract class CA : IA
{
    public int Foo() => 1;
    public abstract string Bar();
}
public class CB : CA
{
    public string Bar() => "hi";
}
public class CC : IA
{
    public int Foo() => 2;
    public string Bar() => "bye";
}
// In assembly A2
using A1;
public static class Extensions
{
    public static string GetBar<T>(this T a) where T : IA => a.Bar();
}
// In assembly A3
using A1;
using A2;
static class Program
{
    static IA GetAFromSomewhere() => ...;

    static void Main()
    {
        var a = GetAFromSomewhere();
        var s = a.GetBar();
        System.Console.WriteLine(s);
    }
}

As I understand it, according to the proposal, if an abstract static member is added to IA, the extension GetBar will now require a non-abstract T, even if CA implements it. Assembly A3 does not compile anymore. Does this make sense? Does it matter if the assembly A2 is recompiled against the new version of A1?

acaly commented 3 years ago

Assembly A3 does not compile anymore. Does this make sense?

Please correct my if I missed anything. I think that's an issue discussed in a LDM: Self-applicability as a constraint. So basically the LDM decided to disallow IA to fulfill IA.

The main question here is what do we do about this? We have 2 paths:

  1. Forbid interfaces with an abstract static from satisfying a constraint on itself.
  2. Forbid access to static virtuals with a type parameter unless you have an additional constraint like concrete.

Option 2 seems weird here. Why would a user have constrained to a type that implements an interface, rather than just taking the interface, unless they wanted to use these methods?

Honestly, for the 2 options raised there, I don't see option 1 to be anywhere less weird than option 2. In my opinion, interface constraints with an abstract static method should be considered as a different thing than normal interface constraints. It's actually asking for an normal interface (used to store the boxed instance) + a TypeHandle variable (or something equivalent, used to get those static methods, although this variable can be inlined when JIT'ed). The interface perfectly fulfills the former (you can box a CA : IA as either CA or IA), but can't provide the latter. The users should use concrete to explicitly specify they also want the latter. The language designers should not silently change the meaning of : IA.

Remember there has already been discussion above in this very same thread on whether the static virtual thing is easy to follow for C# programmers, and it is expected to be

To learn this feature, you basically just have to be told "now it works for static interface methods as well".

But one day suddenly you learn that T : IA is something different than you've learned in non-static methods.

C# has always been an intuitive language (at least from a C++/Java user's perspective), but it is definitely counterintuitive to disallow IA to be IA. If you can't call static methods from an interface (e.g., call IHasStatics.GetValue() for the code in the above LDM link), you shouldn't call T.GetValue() either given T : IHasStatics. We introduced new() for additional constructor requirements, so why not do the same for static members?


Let me add another point. The "Why would a user have ... rather than ... unless they wanted to ..." logic isn't always reliable when designing a programming language. This actually reminds me of the famous Javascript Trinity. The designer could argue "why would a user compare a number with an array, rather than elements in the array, unless they wanted to compare the length?". Is this claim correct? Yes. Is it a good design? No.

acaly commented 3 years ago

Another example showing that it's a bad idea to change the meaning of where A : B and disallow interface to satisefy the constraint of itself:

    public class C1<T1, T2> where T1 : T2
    {
        private static C1<T2, T2> X;
    }

    public interface I2
    {
    }

    public sealed class C2 : I2
    {
        private static C1<C2, I2> X;
    }

Here I2+C2 can be in a different assembly than C1. Currently this code compiles. If we decide to make the change, this should no longer compiles, because C1 uses C1<T2, T2>, which may be a invalid type. Options may be:

  1. Disallow C1<T2, T2> to fulfill the T1 : T2 constraint. Note that this will give an error even if the user never uses static abstract member anywhere in his code, and there is no workaround.
  2. Disallow C1<C2, I2> only if I2 has static abstract members, but for what reason?

I think neither option is better than having a separate constraint like concrete before you can use the static abstract members.

the-avid-engineer commented 3 years ago

Something that came up in the runtime discussion dotnet/runtime#49558 makes me wonder about the language proposal:

Will there be a syntax for referring directly to operators? int.operator+ (for example) doesn't seem to work at the moment. So I'm not sure if T.operator+ would work, too.

Example:

public static T SumAll<T> (IEnumerable<T> seq) where T: IAddable<T> {
        return seq.Aggregate(T.Zero, T.operator+);
}

That feels really dirty. This feels better:

public static T SumAll<T> (IEnumerable<T> seq) where T : struct, IAddable<T> {
        return seq.Aggregate(default(T), (T a, T b) => a + b);
}

At least, for primitives. The .Zero thing seems okay for custom implementations

En3Tho commented 3 years ago

Zero feels like something consciously added, default feels like it could be anything. To me it does feel like a clean functional approach. On the other hand your default is like a screaming warning. What if you have addable class records? Nullref all the way?

kzrnm commented 3 years ago

As a result of discussions about .Zero and.One, IAdditiveIdentity and IMultiplicativeIdentity have been added. https://github.com/dotnet/designs/pull/205#discussion_r625214885 https://github.com/dotnet/runtime/pull/54650

quixoticaxis commented 3 years ago

@naminodarie the current implementation of identity in the PR assumes that there is only one definition of multiplication per type and thus it is fine to assosiate identity with the type itself, not the definition of multiplication. Do you happen to know whether it was discussed?

hez2010 commented 3 years ago

That feels really dirty. This feels better:

public static T SumAll<T> (IEnumerable<T> seq) where T : struct, IAddable<T> {
        return seq.Aggregate(default(T), (T a, T b) => a + b);
}

At least, for primitives. The .Zero thing seems okay for custom implementations

They are not the same, (T a, T b) => a + b would introduce an unnecessary closure allocation.

the-avid-engineer commented 3 years ago

That feels really dirty. This feels better:


public static T SumAll<T> (IEnumerable<T> seq) where T : struct, IAddable<T> {

        return seq.Aggregate(default(T), (T a, T b) => a + b);

}

At least, for primitives. The .Zero thing seems okay for custom implementations

They are not the same, (T a, T b) => a + b would introduce an unnecessary closure allocation.

My main point is that the + operator being accessed as a method instead of as.. you know.. an operator.. feels dirty (not wrong, just dirty). You could make a static method to avoid the closure allocation

CyrusNajmabadi commented 3 years ago

They are not the same, (T a, T b) => a + b would introduce an unnecessary closure allocation.

This lambda doesn't seem to capture anything. So there should be no closure allocation here. You can also use the static modifier to ensure this.

xoofx commented 3 years ago

Hey, while talking about this feature on Twitter, some folks are asking why the mandatory abstract modifier? Why we didn't choose to perform similar (implicit) detection as we do for instance methods by the presence of brackets {...} or not? (even though for the default implementation case for static in interfaces, which as I read in some LDM is a complicated case and might be evaluated in the future)

YairHalberstadt commented 3 years ago

I would suggest that the fact that public abstract M(); and M(); mean the same thing is a bug not a feature - an unfortunate consequence of how interfaces evolved. With static members it's possible to get this right from the start.

hez2010 commented 3 years ago

Hey, while talking about this feature on Twitter, some folks are asking why the mandatory abstract modifier? Why we didn't choose to perform similar (implicit) detection as we do for instance methods by the presence of brackets {...} or not? (even though for the default implementation case for static in interfaces, which as I read in some LDM is a complicated case and might be evaluated in the future)

I'm for it. Statics in interface should be made abstract(virtual) by default, it's consistent with existing interface world, even this will lead to breaking changes. Non-virtual SIM is quite an uncommon pattern even after C# 9 released, there's almost nobody actually using it. I don't think it's worthy to break the consistency in favor of so-called compatibility.

To summarize, this is how non-virtual methods in interfaces already work:

interface IFoo
{
    void Foo(); // abstract
    void Foo() { ... } // virtual
    sealed voif Foo() { ... } // non-virtual
}

Therefore, SIM should be:

interface IFoo
{
    static void Foo(); // abstract static method
    static void Foo() { ... } // virtual static method
    sealed static void Foo() { ... } // Non-virtual static method
}

This doesn't touch any ABI things, so existing libraries using non-virtual SIM will continue working. If users encountered combability issues while upgrading their projects which are using non-virtual static methods in interfaces, either sticking to C# 9 or simply doing find-and-replace to add sealed before non-virtual SIMs.

Otherwise, abstract and virtual keywords in interfaces will make C# really inconsistent and inelegant, also, people will confuse about the semantic at very first time (why interfaces are used for abstraction but static methods in interfaces are not made for it?).

Given virtual SIMs are shipping as preview in .NET 6 and non-virtual SIMs are having quite few users today, it really should be revisited to consist of the existing semantics before it turns to stable.

bernd5 commented 3 years ago

Making something not virtual can increase the performance! If something is virtual we need to look at call-side where the correct implementation is. For non virtual static function it is clear that they can't be overwritten.

hez2010 commented 3 years ago

This doesn't mean we don't have non-virtual static method in interfaces anymore. What I mean is that it should be made consistent with what non-static default non-virtual methods do in interfaces, i.e. adding a sealed before method to indicate it's non-virtual.

Non-virtual SIMs have quite few users today, this breaking change definitely worth IMO.

bernd5 commented 3 years ago

I don't see what the access modifiers have to do with virtual or not. It would be very confusing! In my eyes we should not make static interface members implicitly abstract or virtual. For instance member this is just for backward compatibility (if we could ignore backward compatibility I would like to force a keyword there, too).

xoofx commented 3 years ago

To clarify my question above, about explicit abstract for static vs implicit abstract for instance:

image

hez2010 commented 3 years ago

I don't see what the access modifiers have to do with virtual or not. It would be very confusing!

@bernd5 Interface itself means abstraction, it originally works the same way to abstract class. Therefore, IMO if a method in an interface is not abstract/virtual, it should be decorated with access modifiers explicitly.

bernd5 commented 3 years ago

For interfaces the default access modifier is public. So adding a public access modifier to an interface member changes nothing.

hez2010 commented 3 years ago

It's already the way how non-static methods in interfaces work today:

interface IFoo
{
    void Foo(); // abstract
    void Foo() { ... } // default impl, but it's virtual
    sealed void Foo() { ... } // non-virtual
}

At least SIMs should consist to it.

bernd5 commented 3 years ago

No, public void Foo() is virtual! See: here

Currently the only way to create a non-virtual instance interface function is by marking it private or sealed.

xoofx commented 3 years ago

In my eyes we should not make static interface members implicitly abstract or virtual. For instance member this is just for backward compatibility (if we could ignore backward compatibility I would like to force a keyword there, too).

In language design, it's a tradeoff between explicitness vs implicitness and consistency. I have seen the latter usually being taking over in C#...

The legacy behavior of interface is implicitness for its member declaration:

The very first reaction that I got from folks discovering for the first time this feature was the inconsistency with instance methods declaration.

It might be very unlikely that we will be able to create default implementation for interface static methods as they are introducing compile time/runtime challenges that we don't have an answer for today (see discussion here)

So it's a valid question. Unless I missed something, I don't see the requirement of the abstract modifier clearly explained in the proposal or the design document. It's probably important to document this (in case we don't want to change what has been so far agreed)

hez2010 commented 3 years ago

No, public void Foo() is virtual! See: here

Currently the only way to create a non-virtual instance interface function is by marking it private.

My bad, I meant sealed instead of public: here

kamyker commented 3 years ago

To summarize, this is how non-virtual methods in interfaces already work:

interface IFoo
{
    void Foo(); // abstract
    void Foo() { ... } // virtual
    sealed voif Foo() { ... } // non-virtual
}

Therefore, SIM should be:

interface IFoo
{
    static void Foo(); // abstract static method
    static void Foo() { ... } // virtual static method
    sealed static void Foo() { ... } // Non-virtual static method
}

Looks good but it's not consistent with class:

class Foo
{
static void Foo() { ... } // Non-virtual static method
}

For me this makes the most sense:

 interface IFoo
 {
     static void Foo(); // abstract static method
     static void Foo() { ... } // Non-virtual static method - same as before
     virtual static void Foo() { ... } // Virtual static method
 }

Abstract could be optional in static void Foo(); as it's kind of obvious and follows non-static interface member behavior and appearance.

xoofx commented 3 years ago

Got the answer on Twitter here from @tannergooding and @jaredpar

image

So yep, then abstract keyword is needed and we cannot change that anymore. I don't mind as much as I love this feature 😅

tannergooding commented 3 years ago

With the preview of the feature we are getting feedback on a few channels (https://devblogs.microsoft.com/dotnet/preview-features-in-net-6-generic-math/, https://www.reddit.com/r/csharp/comments/p20xap/preview_features_in_net_6_generic_math/, https://twitter.com/tannergooding/status/1425223277941719040?s=20, https://twitter.com/tannergooding/status/1425319214710104067?s=20, etc).

One of the commonly recurring questions (https://github.com/dotnet/csharplang/discussions/5027) so far relates to invoking static abstract interface members when the concrete type implements the member explicitly.

The current recommendation is for users to create a generic helper method to fill in the functionality:

public static T ParseHelper<T>(string value, IFormatProvider provider)
    where T : IParseable<T>
{
    return T.Parse(value, provider);
}

var result = ParseHelper<double>.Parse("123", null);

However, this is fairly verbose and ideally would be unnecessary. One possible option would be to make the language more concretely understand the CRTP (curiously recurring template pattern) where it relates to TSelf:

public interface IParseable<this TSelf> { ... }
// -or-
public interface IParseable<TSelf>
    where TSelf : this { ... }

// vs today
public interface IParseable<TSelf>
    where TSelf : IParseable<TSelf> { ... }

If the language allowed this, it could then encode in metadata (likely via a hidden attribute) that TSelf must be the implementing type. This would then make "nonsensical" patterns like struct MyStruct : IParseable<int> illegal. It would likewise allow IParseable<double>.Parse("123", null) as the language understands that double is the concrete type that the constraint token before the callvirt should be emitted for.

quixoticaxis commented 3 years ago

@tannergooding if the language may support it, why force generic in the first place? What is the issue with the following?

interface IParseable
{
    this Parse(string value); // TSelf, Self, this, This or anything else that could be reserved
}
tannergooding commented 3 years ago

if the language may support it, why force generic in the first place? What is the issue with the following?

At the IL level, the interface fundamentally needs to be generic. I don't think completely abstracting that is beneficial or clear to the user.

acaly commented 3 years ago
public interface IParseable<this TSelf> { ... }

I like this, but will it block inheritance? For example, with a base class class A : IParseable<A>, will the compiler allow a derived class class B : A, as it implements IParseable<A> instead of IParseable<B>?

pjmlp commented 3 years ago

As one of those guys asking about static abstract, my preference would be just to introduce a new keyword to represent the desired semantics.

C# is going the C++ way in overloading keyword meanings.

Interface methods are already abstract by definition, and static methods cannot be overriden, now try to explain what static abstract does to a C# beginner.

And this is only in the context of C#, then there are the other .NET languages that also need to use libraries that make use of this, or what is the story here regarding CLS and interoperability?

acaly commented 3 years ago

As one of those guys asking about static abstract, my preference would be just to introduce a new keyword to represent the desired semantics.

C# is going the C++ way in overloading keyword meanings.

Interface methods are already abstract by definition, and static methods cannot be overriden, now try to explain what static abstract does to a C# beginner.

And this is only in the context of C#, then there are the other .NET languages that also need to use libraries that make use of this, or what is the story here regarding CLS and interoperability?

I agree. I think the problem is with static, non-abstract members of interfaces. For non-static members of interfaces, classes only need to implement them, and those can be virtual or non-virtual methods (especially when implementing in structs), which seems to be consistent to what static abstract members do. For static abstract members, classes only need to provide an implementation, no matter virtual or non-virtual. Since static methods in classes can only be non-virtual, static abstract members of interfaces effectively requires static non-virtual members.

However, interfaces have been allowing static members that are members of the interface itself instead of as constraints to implementing types. This is against the idea of reusing interfaces to express "shapes".

If we introduce a new keyword to express the concept of "constraints" members of interfaces, this inconsistency can be partially solved. I just call it constraint member here. So non-static members and static abstract members of interfaces are all constraint members, and static non-abstract members are not constraint members. We should recommend using the new keyword for non-static members and static abstract members:

constraint void M1(); //normal non-static member
static constraint void M2(); //static abstract member
void M3(); //same as 'constraint void M3();' but producing a warning (maybe with a conditional switch similar to nullables)
static void M4() {} //static non-abstract member

The point is, when a class implements an interface, it must provide implementations for all constraint members, which makes static and non-static thing symmetric again. It is also better than using 'abstract' as it is now, which has been closely related to 'virtual', while actually implementations may be either virtual or non-virtual. It can also answer why static abstract members in interfaces, unlike non-static abstract members in classes, cannot be directly called: it's because it's just a constraint and is not an actual member of the interface!

EnCey commented 3 years ago

@acaly requiring a keyword in front of every regular interface method is a massive breaking change that I cannot imagine will ever be implemented. Even the fallback of "just" producing a warning if the constraint keyword is missing is highly disruptive. Now we can suggest to use a feature switch (like for nullable), but that complicates the language and may not help if 3rd party libraries don't use it.

Personally I also think this is too verbose. I would much prefer a keyword or combination of them for the new feature.

Interface methods are already abstract by definition, and static methods cannot be overriden, now try to explain what static abstract does to a C# beginner.

@pjmlp I see your point, but I think there are ways to make this work. Interfaces can contain static methods, and beginners know how they work. A static abstract method is a static method without a body – just like the abstract methods you already know. And just like those, it must be implemented in the class that extends the base class or interface.

If they add static virtual methods too, it would "make sense" in the same way: it's like a static abstract method, but you can provide a body for it in the interface.

Of course there is a learning curve, but the additional abstract/virtual keywords have the exact behavior we already know them for, which is a big plus in my opinion. Beginners don't need to learn this straight away, it's like implicit interface methods or co-/contra-variance in generics: there for when you need it, but more niche and not everyone must understand/use it in the beginning

tannergooding commented 3 years ago

C# is going the C++ way in overloading keyword meanings.

@pjmlp, There is no new or additional meaning here. It is a combination of two existing keywords and they preserve their existing meanings.

my preference would be just to introduce a new keyword to represent the desired semantics.

New keywords come with a plethora of problems, including new concepts to keep track of and having to disambiguate the keyword in the compiler. Is new_keyword a type or member name, does it represent some other contextually available identifier?

The existing keywords are unambiguous and always available, if a user wants to use it in an identifier it must be prefixed with @, and as with the first response there is no overloading of meaning on the existing concepts here.

Interface methods are already abstract by definition

That's not true in all languages or in IL and particularly not in C# since 8.0 when Default Interface Methods were introduced. In particular, IL has always allowed representing non-abstract static functions in interfaces.

or what is the story here regarding CLS and interoperability?

With regards to CLS compliance my own view (don't take this as an official stance, etc) is that its not a worthwhile feature to consider. Modern .NET often uses or supports non-CLS compliant types/data and several modern C# features involve things like modreq which are not CLS compliant, but for which the compiler doesn't correctly warn when you don't apply CLSCompliant(false). I don't see continuing trying to support the concept of CLS Compliance as a worthwhile investment, particularly when the largest set of APIs it impacts are simply unsigned data types.

acaly commented 3 years ago

@EnCey

I understand that there would be larger cost with new keywords. The question is only whether it worth it.

If they add static virtual methods too, it would "make sense" in the same way: it's like a static abstract method, but you can provide a body for it in the interface.

If static method will be able to be virtual, I will be convinced that static abstract is a good idea. Now it seems that static virtual will never be possible, and static abstract is using a completely different mechanism. This mechanism is different from non-static abstract methods, which makes the naming counterintuitive. This mechanism also introduces glitches that interface I cannot be used in 'where T : I'. By all means, it's not like a method of the interface at all.

acaly commented 3 years ago

@tannergooding

virtual means that the defining type does provide an implementation, but a deriving type may also override that implementation with its own.

This is inaccurate. If you suggest this definition, then I think all static members are virtual, because you are free to write a new one, just like for static abstract members. This also why static abstract member is confusing to me (and to beginners as mentioned above): non-static, abstract members are usually implemented as (and implicitly) virtual, which is not the case for static abstract members.

pjmlp commented 3 years ago

@tannergooding Thanks for the deep explanation.

I still have my doubts, but do acknowledge it is a bit bike shedding.

As for the interoperability part, while I appreciate the C# improvements of late, they seem to come to the expense of turning CLR into the C# Language Runtime, with VB, F#, C++/CLI getting 2nd or 3rd place treatment, if at all, as some newer frameworks become unaccessible to them other then we having to write wrapper libraries in C#.

Just as general feedback from the trenches.

HaloFour commented 3 years ago

@acaly

then I think all static members are virtual, because you are free to write a new one, just like for static abstract members

No, that's defining a completely new and independent method that happens to have the same name. It's not virtual unless there is virtual dispatch involved, and that's what the new feature is about.

acaly commented 3 years ago

@acaly

then I think all static members are virtual, because you are free to write a new one, just like for static abstract members

No, that's defining a completely new and independent method that happens to have the same name. It's not virtual unless there is virtual dispatch involved, and that's what the new feature is about.

Yes that's excatly how we C# programmers understand virtual. But, consider this example:

using System;
public interface I
{
    static abstract void M();
}
public class C1 : I
{
    public static void M()
    {
        Console.WriteLine("C1");
    }
}
public class C2 : C1
{
    public static void M()
    {
        Console.WriteLine("C2");
    }

    private static void CallM<T>() where T : I
    {
        T.M();
    }

    public static void Main()
    {
        CallM<C2>(); //Which method will be called?
    }
}

C2 has another M() that will be called by CallM. It effectively overrides the base class's implementation in the case of using static abstract. (Or maybe I'm wrong about this, but it will be also confusing if not so.)

HaloFour commented 3 years ago

@acaly

C1.M implements I.M. C2.M hides C1.M and does not implement I.M. This is the exact same behavior you get with instance members, and the compiler warns about it and suggests adding new to C2.M.

acaly commented 3 years ago

and does not implement I.M

I did not know about this. Then it makes much sense to me.