dotnet / csharplang

The official repo for the design of the C# programming language
11.13k 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

leandromoh commented 3 years ago

About 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).

I would expect I could define a default implementation for static members, because

CyrusNajmabadi commented 3 years ago

@leandromoh Then you would not mark it 'abstract'. It would be a non-abstract static interface member.

leandromoh commented 3 years ago

@leandromoh Then you would not mark it 'abstract'. It would be a non-abstract static interface member.

I see, great!

HaloFour commented 3 years ago

@leandromoh

I would expect I could define a default implementation for static members

That is mentioned under "Default implementations" and I think describes the concept of virtual static members.

333fred commented 3 years ago

leandromoh

I would expect I could define a default implementation for static members

That is mentioned under "Default implementations" and I think describes the concept of virtual static members.

Yes, in LDM we discussed being able to put both virtual and abstract on static members. The difference there would be the same as in abstract classes: abstract does not have a body, virtual does.

alrz commented 3 years ago

My understanding is that this would be only available through constrained generics.

Would we somehow be able to define virtual extension methods?

interface ITokenExtensions {
    abstract static bool IsLiteral(this Token tk);
}
class C<T> where T : ITokenExtensions {
    // ignore the fact that currently using static excludes extensions and type-level usings don't exist
    using static T; 
}

Though I think shapes would be better suited for this case.

shape SToken {
    bool IsLiteral { get; }
}
implement SToken for MyToken {
    bool IsLiteral { get { .. } }
}
interface SToken<TThis> {
    abstract static bool get_IsLiteral(TThis @this);
}
struct SToken_for_MyToken : SToken<MyToken> { 
    public static bool get_IsLiteral(MyToken @this) { .. }
}

And kind of covers "extension everything" as well.

Trayani commented 3 years ago

It might be too soon to ask, but in case this feature gets added to the language, would it makes sense to add some general-purpose interfaces-and-their-implementations to BCL (such as IAddable for numeric types?).

@alrz , I believe your example should be indeed covered by shapes which revolve around implicit implementation. static abstract will still require explicit implementation.

CyrusNajmabadi commented 3 years ago

@Trayani yes. This was discussed as part of the design.

HaloFour commented 3 years ago

@CyrusNajmabadi

yes. [IAddable for numeric types] was discussed as part of the design.

Is it known if the approach being considered to support this by the runtime would be a zero-cost abstraction? I understand that the BCL considered adding numeric interfaces quite some time ago but they ended up being considered unwieldy and to have too much performance overhead so they got axed.

alrz commented 3 years ago

Is it known if the approach being considered to support this by the runtime would be a zero-cost abstraction?

Couldn't that all be runtime intrinsics? So in practice all of it should compile away at runtime.

Also I think the actual impl would be more involved than that. Looking at rust implementation (https://doc.rust-lang.org/src/core/ops/arith.rs.html) it could turn out to be something like IAddable<TThis, TRhs, TOutput>. you need a few other features to make that less unwieldy still (default constraints, associated types, etc).

orthoxerox commented 3 years ago

@HaloFour I don't see why it can't be a ZCA for struct types. They get specialized copies of the generic methods, so baking the right implementation into each copy should be straightforward.

MgSam commented 3 years ago

Not having virtual/abstract static members in classes ship at the same time will present a weird scenario where you have to move static members to interfaces if you want to abstract over them. It breaks some of the symmetry present between classes/interfaces.

If this feature and default implementations are too costly to design because of static virtual members calling each other, then a reasonable compromise would just be to ban them calling each other in the first version of the feature and revisit in the future if there's interest.

p-lindberg commented 3 years ago

I came to think of a situation where this feature would be handy, and figured I'd contribute it to the discussion as another reason to consider this feature:

When writing generic methods, you sometimes face the issue that you require some information about the generic type itself (as opposed to an instance of the type), and currently there is no great way to enforce that that information exists. For instance, say you have a generic method that places some type of resource (i.e. a generic type) into a cache, and each type of resource should define a key that it should be cached under.

Some options we have today:

If interfaces could have abstract static properties and methods, then we could simply place static abstract string CacheKey { get; } on IResource and treat resources generically even when we don't have an instance available.

I guess my point is that having this feature would allow us to write very nice generic APIs that communicate very clearly to the client how to use them, while at the same time allowing us to write much more concise code that can deal with a broad range of types in a generic way. I therefore think this would be a very valuable addition the language.

I also have a feeling that this has the potential to enable a lot of new powerful meta programming, and that's always fun.

Thaina commented 3 years ago

Want to +100 for this if possible

pakoito commented 3 years ago

IMonoid

poke commented 3 years ago

Just as some clarification since the opening motivational example was mostly based on operators: Will this allow interface declarations like the following?

public interface IAsyncFactory<T>
{
    abstract static Task<T> CreateAsync();
}

public interface IExampleStrategy
{
    abstract static bool IsEnabled(string foo);
    void DoStuff(string foo);
}
333fred commented 3 years ago

Just as some clarification since the opening motivational example was mostly based on operators: Will this allow interface declarations like the following?

As proposed, yes, you could define those abstract statics. Math may be the motivating example, but factories will also be possible.

zahirtezcan commented 3 years ago

I do hope these don't result in boxing or virtual-calls. Otherwise using a generic method on an array of values would be a pitfall to avoid for all newcomers.

Also, why not just call it as static interface such as a static class and make it easier instead of all static abstract typing for lazy people like I am :)

HaloFour commented 3 years ago

@zahirtezcan

Also, why not just call it as static interface such as a static class and make it easier instead of all static abstract typing for lazy people like I am :)

A single interface could have both required instance and static members.

CyrusNajmabadi commented 3 years ago

Also, why not just call it as static interface such as a static class and make it easier instead of all static abstract typing for lazy people like I am :)

A static interface would require all members to still be declared as static, just like static classes do.

Kant8 commented 3 years ago

Also, why not just call it as static interface such as a static class and make it easier instead of all static abstract typing for lazy people like I am :)

A static interface would require all members to still be declared as static, just like static classes do.

I'd say this can be a nice addition anyway. Same as for classes, by default interfaces can contain both static and instance methods, but static interfaces can have only static methods. Maybe it'll be additional hint for compiler to properly get rid of boxing in such cases, when you explicitly notify that you don't need any instance information while using this interface.

lambdageek commented 3 years ago

Something that came up in the runtime discussion https://github.com/dotnet/runtime/pull/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+);
}
BreyerW commented 3 years ago

@lambdageek Recommended way to implement operators was always that you provide normal method that actually implements operator and call that method in operator. So for your example you would implement operator and static int Add(int,int) and call the latter for aggregate. Not ideal but this workaround is good enough i think

Thaina commented 3 years ago

One thing I concern is the composite nature of many static operator

Some object can + - * / (number type) Some object can only + - Some object can only + * (remember seeing this once, a class that can't - or /) Some object can only + (string) Some object can only + - and all related type are not the same (DateTimeOffset + TimeSpan return DateTimeOffset but DateTimeOffset - DateTimeOffset return TimeSpan)

And this is only little example, and not only operator, many static function in BCL of the same pattern too

Do we have a way to define these general interface?

hez2010 commented 3 years ago

One thing I concern is the composite nature of many static operator

Some object can + - * / (number type) Some object can only + - Some object can only + * (remember seeing this once, a class that can't - or /) Some object can only + (string) Some object can only + - and all related type are not the same (DateTimeOffset + TimeSpan return DateTimeOffset but DateTimeOffset - DateTimeOffset return TimeSpan)

And this is only little example, and not only operator, many static function in BCL of the same pattern too

Do we have a way to define these general interface?

I think it's better to create some types in BCL for existing primitive types too. eg. IAddable and etc.

GSPP commented 3 years ago

What use cases are there except generic math?

Generic math is an extremely rare requirement in practical applications.

hez2010 commented 3 years ago

What use cases are there except generic math?

For example, when you want to create something like numpy.

Thaina commented 3 years ago

What use cases are there except generic math?

Generic math is an extremely rare requirement in practical applications.

Totally and ultimately enormous

Imagine a factory method. We can define interface with static T Create<T>(any parameter) and create a dictionary for any object that declare this static Create function with auto creation

We can define a type that do nothing. But contains only static method that could be switch and replace. Such as json parser and type conversion. Instead of instantiate object it will just use static method directly

We can define a static property or function that describe the class itself (a string or enum for describing the class). Or forcing a class to declare object we want to use (such as dictionary of its own type)

Generally this feature would be great for create framework. There could be various possibility to do something with the class itself even without instance of that class. In the past we have to workaround with instance interface member. Which is not perfect

omikhailov commented 3 years ago

Static virtual methods where? In interfaces? Are you delirious?

hez2010 commented 3 years ago

Static virtual methods where? In interfaces? ~Are you delirious?~

It's necessary for operator abstraction.

omikhailov commented 3 years ago

It's necessary for operator abstraction.

Well, maybe there is a problem that can be solved with interfaces, but when you have a problem with operations, you usually thinks first about operations, not a stuff that is not related to them directly.

So, questions to ask before even starting thinking in that direction would be:

It's shoking that discussion jumped to the last point right from the first post.

YairHalberstadt commented 3 years ago

@omikhailov

It's shoking that discussion jumped to the last point right from the first post.

There have been literally years of discussions around this issue, and the design has steadily evolved as more clarity has been raised on what the design is trying to achieve and what the tradeoffs are.

For example check out

https://github.com/dotnet/csharplang/discussions/164 https://github.com/dotnet/csharplang/issues/1711 https://github.com/dotnet/csharplang/blob/main/meetings/2020/LDM-2020-06-29.md https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-02-08.md

You've also said

Static virtual methods where? In interfaces? Are you delirious?

but have never explained why doing so is so terrible.

I'll also point out that calling someone delirious breaches the Code of Conduct. Please treat everyone with respect. Thanks!

leandromoh commented 3 years ago

from issue's summary

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.

One doubt about abstract static members: will be optional the modifiers abstract/virtual? as well it is today for instance members

333fred commented 3 years ago

One doubt about abstract static members: will be optional the modifiers abstract/virtual? as well it is today for instance members

https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-02-08.md#syntax-clashes

leandromoh commented 3 years ago

@333fred Thanks

markm77 commented 3 years ago

I think this feature is great! One use of an interface is to represent a group of similar classes (e.g. in generic contexts) using class overrides to capture differences. So far we could not capture class static commonalities/differences via an interface but this is getting fixed!

I want to make a comment about default implementations for interface statics (discussed recently).

Forgetting about structs for a moment, my viewpoint is that interfaces are essentially abstract classes without storage. And therefore I would urge the team to consider the considerable benefits of allowing concrete classes to properly inherit default implementations of interface statics. (Providing of course there is no name clash arising from multiple inheritance.) So that, using the example from the LDM notes, _ = c != c; just works.

I realise this breaks the existing concept that classes can't inherit interface default implementations. But this "hiding" of default implementations makes life difficult in quite a few ways. For example when calling an interface default implementation (the "base" method) when specialising it at class level. And in generic contexts you can end up with some really ugly casts/types at call-sites (here's an example I wrote where I was forced to call static method GetLocalAsync() on ISupportsFluentGetLocal<TEntity, TPublicQuery, TPublicLocalResponse> rather than on the so much more straightforward TEntity).

viniciusjarina commented 3 years ago

I understand the need of adding new features to C#

But soon we will make it so convoluted as C++ (and scary for newcomers)

CyrusNajmabadi commented 3 years ago

@viniciusjarina Please keep things civil. Gifs like that do not help teh conversation. Please treat this repo as a professional setting and remember to follow the .net code of conduct: https://dotnetfoundation.org/about/code-of-conduct

Importantly:

viniciusjarina commented 3 years ago

@CyrusNajmabadi Ok, I removed the GIF, but I am saying my honest opinion. By making C# so complex we are helping like 0.01% of the users, and moving newcomers away :(

CyrusNajmabadi commented 3 years ago

@viniciusjarina Giving honest opinions is fine. Just please do so in a constructive fashion. Gifs like that are not conducive to being able to have positive and constructive discourse. Thanks! :)

CyrusNajmabadi commented 3 years ago

By making C# so complex we are helping like 0.01% of the users, and moving newcomers away :(

I would not expect newcomers to need to use these features. This would just let authors write more sensible APIs that performed faster. User would simply see scenarios that they don't have today become available. For example, being able to do things like efficiently add arrays of disparate types with just a single helper.

HaloFour commented 3 years ago

@viniciusjarina

How productive is that statement, though? What programming languages that are still actively in use have stopped evolving and adding new features? I'd posit none of them.

Thaina commented 3 years ago

@viniciusjarina

By making C# so complex we are helping like 0.01% of the users, and moving newcomers away :(

I can confidently state that this feature is totally not the case. This feature might not be handled by most user. But everyone will benefit from it with almost all framework and library will utilize this feature in some way. So many logic can be reused and standardized with this feature allowed. It allow so many workaround to possibly implemented in replace of many other feature

Not to mention this feature are not really that complex anyway

CyrusNajmabadi commented 3 years ago

we are helping like 0.01% of the users

Interestingly, this statement is probably true. However, importantly, the small percent helped here are library/platform authors, who can use this to significantly and substantively improve things for all users. This is similar to all the ref work. It's likely only interacted with by a small subset of users. But that subset gets enormous value from that that makes it way out to everyone else using .net.

viniciusjarina commented 3 years ago

I didn't mean to offend or cause any trouble.

I was only criticizing this need of adding dozens of new features every release of C#.

I know some of you might say this won't affect newcomers because will be a feature used by the framework.

But since is part of the language, when someone start to learn C# it will eventually hit this topic.

I guess if is a change that will be used by the Framework only, maybe shouldn't be in the language itself that will be used by millions of people.

CyrusNajmabadi commented 3 years ago

I guess if is a change that will be used by the Framework only

It won't just be used by the framework. It will be used by library developers. They need the language to support them here so they can express these abilities, and so that the benefits are not just restricted only to the runtime.

I know some of you might say this won't affect newcomers

I do not see why this would be particularly impactful to newcomers. Anymore so than something like 'ref structs', or pointers, etc. They are very advanced topics that i would not expect newcomers to be introduced to.

it will eventually hit this topic.

If the newcomer is interested enough to get to this point in the language, i woudl say they're likely not a newcomer anymore. Rather, they're a very interested user that really wants to dive into all the complexity and power the language has available. In that case, more power to them :)

I was only criticizing this need of adding dozens of new features every release of C#. ... that will be used by millions of people.

Newcomers are one segment of our user base. As you said, we have millions of users. There are strong needs of that ecosystem and community to solve real pain points and break through limitations causing significant pain and perf problems in the library ecosystem. We have to balance these needs. And we have balanced them to slow and gradual improvement in the language. Not locking things down because we are worried this is the straw that breaks the camel's back with newcomers.

CyrusNajmabadi commented 3 years ago

I didn't mean to offend or cause any trouble.

That's fine. Just please keep in mind the code of conduct, and remember to keep things respectful. Thanks! :)

BreyerW commented 3 years ago

@viniciusjarina and btw its not like lang and runtime team doesnt move things to API rather than as lang feature whenever they think feature doesnt deserve lang support. Eg. we have Unsafe class with various methods that are possible via CLR but not via c# syntax like ref nulls. Or some of RuntimeHelpers and the like.

omikhailov commented 3 years ago

For example, being able to do things like efficiently add arrays of disparate types with just a single helper.

What stops them now? If some hypothetical library author wants to sum apples with oranges, he can model this and much more in a normal object-oriented style. If this interface craziness continues, it will turn out that for the sake of few people who are not doing well with OOD, it will be allowed to rudely violate the basic principles of OOP by mixing implementation with interfaces.

CyrusNajmabadi commented 3 years ago

What stops them now?

Efficiency.

If this interface craziness continues

There's nothing crazy here. This sort of approach has been wll trod in many other languages and domains. The idea that a type can specify this sort of behavior has been around much longer than i've been alive :)

it will be allowed to rudely violate the basic principles of OOP by mixing implementation with interfaces.

I don't know what you're describing here. It sounds like a complaint about Default-Interface-Members (already shipped), not static-abstract-members-in-interfaces (this proposal).