dotnet/csharplang

[Proposal]: Static abstract members in interfaces

MadsTorgersen opened this issue ยท 579 comments

Static abstract members in interfaces

  • Proposed
  • Prototype: Not Started
  • Implementation: Not Started
  • Specification: Not Started

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

  • "static abstract" is a new concept and will meaningfully add to the conceptual load of C#.
  • It's not a cheap feature to build. We should make sure it's worth it.

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:
- This would have to be written out every time. Having a named constraint seems better.
- This is a whole new kind of constraint, whereas the proposed feature utilizes the existing concept of interface constraints.
- It would only work for operators, not (easily) other kinds of static members.

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:

  • Operators == and != as well as the implicit and explicit conversion operators are disallowed in interfaces today. Should they be allowed?
  • Should the qualifying I. in an explicit operator implenentation go before the operator keyword or the operator symbol (e.g. +) itself?
  • Should we relax the operator restrictions further so that the restricted operand can be of any type that derives from, or has one of some set of implicit conversions to the enclosing type?

Design meetings

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

  • abstracts classes allow default implementation
  • static interfaces allow default implementation, since C#8
  • in some cases is very convenient to define default implementation, for example if we define an interface to check equality, one can let pending of implementation == static operator and as default implementation for != just negate the return of ==

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

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

I see, great!

@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.

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

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.

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.

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

@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

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

@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

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.

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:

  • Define an interface IResource with the string CacheKey { get; } property. However, then you could only access the key if you have an instance of the resource, so you are constrained to only putting resources into the cache, not taking them out, as then you don't have an instance yet.
  • Define attributes on all resources which contains the cache key. This is suitable for this type of type meta-data, but there is no way to enforce that these attributes exists, and therefore no way to communicate to a client of an API that they need to define the attribute.
  • More dynamic solutions like registering key/type pairs in a dictionary. Not much different from using attributes.
  • Abuse of generics and ad-hoc types: IResource<TKeyProvider> where TKeyProvider : KeyProvider, new() and public abstract class KeyProvider { public abstract string Key { get; } }. In other words, require there to exist a class whose only purpose is to specify a key for each resource.
  • Obviously, in this scenario, a simple out would be to use the type name as the key, but that's beside the point.

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.

Want to +100 for this if possible

IMonoid

poke commented

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);
}

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.

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 :)

@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.

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

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.

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+);
}

@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

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?

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

What use cases are there except generic math?

Generic math is an extremely rare requirement in practical applications.

What use cases are there except generic math?

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

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

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

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

It's necessary for operator abstraction.

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:

  • Isn't it something wrong with operations in C#?
  • If yes, is it a real proplem or just a minor issue? Anybody cares?
  • If yes, how operations can be improved? Maybe, their logic can be encapsulated in classes instead of just methods? Any other ideas?
  • And only when there is no way to improve operations, it makes sense to consider other options.

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

@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

#164
#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!

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

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

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

I understand the need of adding new features to C#

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

@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:

  • Being respectful of differing opinions, viewpoints, and experiences
  • Giving and gracefully accepting constructive feedback

@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 :(

@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! :)

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.

@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.

@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

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.

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.

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.

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! :)

@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.

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.

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

Here's another perspective on this. I'm a newcomer to C# (one and a half year's professional experience). Before this I wrote a backend (server) library for Swift in order to share code with an iOS app. (Yes, someone tried that.๐Ÿ˜€) But market demand led to a C# port of my backend library and to be honest, on the whole, I've greatly enjoyed my experience with C#. So much so I think for my next mobile app I'll try Xamarin (I'm still a fan of server/client code re-use.)

But there is one area where Swift is noticeably more clean and much less verbose compared to C# - and that is when writing code that uses a lot of generics. Two major reasons for this in my mind are (a) Swift allows abstracting a lot more things in interfaces ("protocols") such as static properties, static methods and constructors with arguments (none of which can be abstracted in C#), and (b) Swift interfaces support existential types including the Self type (makes a lot of generic code much less verbose and vastly more readable and maintainable).

I see this feature as part of a set of features which will help C# catch up in this area and make it a class-leader for generics with massive application areas including of course numerics which is a big motivator.

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.

Do we have an issue for that? Because that would be incredibly useful. That's an important part of metaclasses, which I've been requesting for years now. Class-scope virtual methods (they're not static methods because they do necessarily have a this parameter; it's just a reference to a class rather than an object instance) are very helpful for a lot of use cases, including providing a far more elegant way (statically verifiable at compile time, no Reflection required, etc) to do many of the same things that are commonly done today with Attributes.

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:

  • This would have to be written out every time. Having a named constraint seems better.
  • This is a whole new kind of constraint, whereas the proposed feature utilizes the existing concept of interface constraints.
  • It would only work for operators, not (easily) other kinds of static members.

On the other hand, the current proposal would not work for any built-in types (or any types at all, really!) without explicitly modifying them to add an interface to them. Operator constraints and structural constraints in general (ie. "shapes" done right; not the awful hack that is the current proposal) would be a much more useful way to solve the problem than a hack of interfaces. (Which is basically shapes done wrong, coming at us from a slightly different angle.)

without explicitly modifying them to add an interface to them.

We will likely be explicitly adding these interfaces to types to support this functionality.

coming at us from a slightly different angle

These are complimentary efforts (which is why we feel comfortable breaking this out now). One is about beefing things up in the world where you have the interfaces and the type opts into it at definition time. The other is for when you have the structural shape you need, and you can apply that to things even after the fact in a way that still retains important things like perf.

We will likely be explicitly adding these interfaces to types to support this functionality.

Yes, and that's the problem: under this proposal, anything you didn't think to explicitly add -- all the non-obvious cases that a small minority of users will still end up needing -- simply won't be available. Operator constraints makes that a non-issue.

all the non-obvious cases that a small minority of users will still end up needing -- simply won't be available.

Yes. As i said, this is complimentary with shapes. See the last part of my post:

The other is for when you have the structural shape you need, and you can apply that to things even after the fact in a way that still retains important things like perf.

These approaches allow us to get this working for both directions. One where the type author intentionally designs this in, and the other for when you're coming to an API that didn't do that.

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.

This is not even as crazy as DIM. This feature is totally natural and should just supported from the start

I thought more and did some experiments regarding class non-inheritance of default interface methods (DIMs).

Amazingly it seems in generic contexts classes can inherit instance DIMs but not static DIMs (see below).

It would certainly be great if the commented-out line below were made to work as part of this feature. (Leads to more clear and less verbose code since in practice IVar often has generic parameters).

        public interface IVar
        {
            string DefaultInstanceProperty => "instanceProperty";
            static string DefaultStaticProperty => "staticProperty";
        }

        class MyGenericClass<TVar> where TVar : class, IVar
        {
            public MyGenericClass(TVar var)
            {
                _ = var.DefaultInstanceProperty;  // works (!): instance inherits property without cast to IVar
                //_ = TVar.DefaultStaticProperty; // doesn't work: only accessible via IVar
            }
        }

@markm77 this will work, and is one of the primary scenarios this language feature is intended for :)

@CyrusNajmabadi Thanks! - sounds great.

In terms of static virtuals with default implementation, this is one main setting where I would use them. The other would be directly off the interface type (IVar) which I don't think will be possible at the moment according to the LDM notes (April 5). Will be interesting to see the conclusion there.

@CyrusNajmabadi @markm77 we are not planning on touching non-virtual static methods accessed on a type parameter (Mark's example), nor are we currently planning on supporting default implementations in virtuals.

@333fred Sorry, i thought the example was implying that it was virtual. Not sure what it would mean (or what purpose it would serve) otherwise.

In terms of DIM, i didn't think we needed to support it. Wouldn't this just fall out? I would not have thought DIM needed special support for this case.

Since it has come up a few times...

For anyone interested in discussing this from the libraries side of things (rather than the language), dotnet/designs#205 tracks the initial rough draft of the API surface we're looking at exposing.

Still lots of thought, design to do here, but this give that first pass that should help give a general idea of how the .NET libraries might be extended. Feedback is welcome, particularly where you feel something might be blocked or limited by the current rough draft.

In terms of DIM, i didn't think we needed to support it. Wouldn't this just fall out? I would not have thought DIM needed special support for this case.

Not sure what you mean. Nothing can fall out here: this is the discussion about how the DIM would know the actual type it was invoked on. We had a theory that we could make it work if we required that DIMs can only be invoked on a type parameter, but came to the conclusion that it wasn't particularly useful.

Thanks for comments. To be clear, my example was about the case of a non-virtual static DIM today. I sometimes use these to capture common (simple) static functionality (even though they can't be overriden). But I get that this and virtual static DIMs will not be addressed now.

alrz commented

Accessing static abstract interface members

I think accessing through objects would be also useful here.

instance.StaticMethod() // not allowed today
instance.AbstractStaticMethod() // allow?

Where instance is an interface, class, or struct.

@alrz how would that work? What would codegen be?

alrz commented

I don't think it would be any different than T.M() where T is the declaring type of M.

alrz commented

I guess if we require T to be statically known (through generics or otherwise), that wouldn't work. just figured there is no virtual dispatch on abstract static method calls and those would indeed only be usable through type parameters.

kzrnm commented

I am concerned that it may be difficult to use casting operator.

For example,

interface IExplicitCastable<TSelf, TOther> where TSelf : IExplicitCastable<TSelf, TOther>
{
    static abstract explicit operator TOther(TSelf value);
}
interface IExplicitCastableRev<TSelf, TOther> where TSelf : IExplicitCastableRev<TSelf, TOther>
{
    static abstract explicit operator TSelf(TOther value);
}

public static TOther[] Cast<T, TOther>(T[] ts) where T : IExplicitCastable<T, TOther>
{
    var result = new TOther[ts.Length];
    for(int i=0; i < ts.Length; i++) result[i] = (TOther)ts[i];
    return result;
}
public static T[] CastRev<T, TOther>(TOther[] ts) where T : IExplicitCastableRev<T, TOther>
{
    var result = new T[ts.Length];
    for(int i=0; i < ts.Length; i++) result[i] = (T)ts[i];
    return result;
}

Cast<IntPtr, long>(new IntPtr[0]); // OK
CastRev<IntPtr, long>(new long[0]); // OK
Cast<long, IntPtr>(new long[0]); // NG: Int64 doesn't have Int64 to IntPtr operator.
CastRev<DateTimeOffset, DateTime>(new DateTime[0]); // NG: DateTimeOffset doesn't have explicit DateTime to DateTimeOffset operator, but have implicit operator.

Could you please list "type trait" or "type interface" as an alternative?

In my humble opinion, the current implementation of interfaces already has lots of questionable features and limitations, for example, the inability to seal the explicit interface implementation makes the following pattern a no go:

public interface IReadOnlyOuter
{
    public IReadOnlyInner Inner { get; }
}
public interface IOuter : IReadOnlyOuter
{
    public new IInner Inner { get; set; }
    /* sealed is not valid here for some reason */ IReadOnlyInner IReadOnlyOuter.Inner => Inner;
}

My point is that the current interfaces mechanism covers the contracts applied to instances of a type and is already complex enough to be difficult to understand. Why add a separate responsibility of providing contracts for types themselves to the same concept? Why not create a separate concept, let's say, type interface:

public type interface IAddable<T>
{
    public T operator+(T x, Ty);
}

I personally highly doubt that this feature would have lots of applications. It sounds cool on paper, and the question of how to write generic arithmetic always arises when working in C#, but I would dare to say that rich type systems do not come up often in production C# code (both libraries and applications).
Bottom line: this proposal looks like another super-localized-single-problem-solving hack.

@quixoticaxis

for example, the inability to seal the explicit interface implementation makes the following pattern a no go:

Explicit interface implementations are already sealed.

Why not create a separate concept, let's say, type interface

Because it is not a separate concept. An interface describes the members that the consumer can call on the type. Adding static members doesn't change that definition. It's an explicit goal to allow an interface to be able to describe both static and instance members. Having two separate constructs here would preclude that.

I personally highly doubt that this feature would have lots of applications.

Generic arithmetic is the primary application, one developers have been asking for since generics were introduced. Static interface members are the first step in enabling that in the language and runtime.

@HaloFour

Explicit interface implementations are already sealed.

Could you elaborate on it? It's enforced neither syntactically nor semantically, AFAIU.

public interface I0
{
    public long Value { get; }
}

public interface I1 : I0
{
     long I0.Value => 1L;
     public sealed long WorksForMethodsThough() => 0L;
     public sealed long AlsoWorksForProperties { get { return 1L; } }
}

public interface I2 : I1
{
    long I0.Value => 2L;
}

public class DataWithValue : I2
{
    public long Value => 4L;
}

public class DataWithExplicitValue : I2
{
    public long I0.Value => 5L;
}

public class DataWithoutValue : I2
{
}

Because it is not a separate concept. An interface describes the members that the consumer can call on the type. Adding static members doesn't change that definition. It's an explicit goal to allow an interface to be able to describe both static and instance members. Having two separate constructs here would preclude that.
Generic arithmetic is the primary application, one developers have been asking for since generics were introduced. Static interface members are the first step in enabling that in the language and runtime.

Unless I miss something, the static interface members are meaningful only for generic types, and there are many much more serious limitations of C# generics like, to name a few, having no specific constructor constraints, having only partial reflection support for type arguments' constraints, no TSelf type, having already defined default semantics that make an option to have a meaningful IDefault interface obsolete, no way to create constructs like IContainer<T> : IDisposable only when T: IDisposable, but not when T is not, no support for constant generics, and the list goes on. Given the runtime nature of C# generics, some of the limitations could be overcome, others cannot. IMHO, the metaprogramming in C# is very weak, so the game is almost always not worth the candle. So what is the final goal? Or at least what is the global direction?

Don't get me wrong, if this is a needed hacky solution for a single problem, then why not?

I personally would prefer the language to be fixed after the application of the previous hacky solutions (for example, splitting the scheduling, the state machine driver, and the state machine to all be explicitly controlled independently instead of the current implementation of async that merges everything in one implicit state, or unifying the behavior of void and non-void partial methods).

@quixoticaxis

Could you elaborate on it? It's enforced neither syntactically nor semantically, AFAIU.

Ah, I see, you mean specifically with interfaces that provide default explicit implementations on members of inherited interfaces. I'm assuming that use case wasn't considered thus the language did not implement a feature to support that. The actual member in that case is still sealed (as in the signature of the emitted methods are final in IL) but given that default implementations are filled in by the runtime that the virtual slot itself remains available to be overridden.

Unless I miss something, the static interface members are meaningful only for generic types, and there are many much more serious limitations of C# generics

You're right, and the team is interested in addressed a wide array of these limitations in an epic often referred to as "shapes" or "roles", which seek to greatly expand on what you can do with generics and generic constraints. Much of it is still up in the air but support for static members is seen as one of the core runtime building blocks to building out the larger language features. I'd suggest that if you have use cases that may or may not be covered by those projects that it'd be worth bringing them up so that the team can understand them and to take them into consideration.

@HaloFour thank you for the links.

In my humble opinion, the current implementation of interfaces already has lots of questionable features and limitations, for example, the inability to seal the explicit interface implementation makes the following pattern a no go:

If you want a "sealed explicit implementation", you can do that easily enough as an extension method for the interface. Put it in the same namespace alongside the interface declaration, and it will basically look to a third party as if it's an explicit implementation that can't be overridden.

@masonwheeler

If you want a "sealed explicit implementation", you can do that easily enough as an extension method for the interface. Put it in the same namespace alongside the interface declaration, and it will basically look to a third party as if it's an explicit implementation that can't be overridden.

Maybe I misunderstand what you mean, but I cannot see how extension method would work for me. I want to have IX : IReadOnlyX, IY: IReadOnlyY, and IZ: IReadOnlyZ where all read only interfaces have only get accessors that return read only interfaces, while read-write interfaces provide setters under the same names and use read-write types instead. It works (code above), but it needs sealed explicit default methods to prevent wrong implementation of interfaces. It enables treating the same instance as either mutable or readonly all the way through references.

to prevent wrong implementation of interfaces.

To me, that defeats the purpose of an interface. The author of it cannot dictate that. If you want that, then an abstract class is appropriate. BUt for an interface, trying to restrict this sort of thing doesn't make much sense to me.

@CyrusNajmabadi

to prevent wrong implementation of interfaces.

To me, that defeats the purpose of an interface. The author of it cannot dictate that. If you want that, then an abstract class is appropriate. But for an interface, trying to restrict this sort of thing doesn't make much sense to me.

In general I would agree, but the example I described is a way to hack the limitations of the language, because there is only one member conceptually and the issues arise only because C# does not allow extending properties in interfaces with setter the same way you can extend 'get_X 'method with a paired 'set_X' method, and C# does not allow overriding interface methods (namely with more derived type).

It's not strictly a huge exaggeration to say that an abstract class containing only methods all of which are abstract is conceptually the same as having an interface. I can write:

public abstract class IReadInner
{
    public abstract long Get_Value();
}

public abstract class IReadWriteInner : IReadInner
{
    public abstract void Set_Value(long value);
}
    
public abstract class IReadOuter
{
    public abstract IReadInner Get_Value();
}

public abstract class IReadWriteOuter : IReadOuter
{
    public abstract override IReadWriteInner Get_Value();

    public abstract void Set_Value(IReadWriteInner value);
}

Why cannot I do the same with interfaces? My code is simply hacking the same behavior in. As far as I can see, there is no fundamental reason why it should not be possible.

And at the end of the day, I suppose, it's all a way to emulate mutable and immutable references that can prevent the whole class of errors in compile-time.

@HaloFour thank you again for the links, I've read about shape proposal before, but it seems that the discussions moved forward. I suppose, I can understand the currently proposed feature if it is meant to be a part of a broader one, and I can only hope that the broader upgrade would be implemented in time, because, as I've written above, this proposal itself (if weighted out of context) seems to me like the one which would add cognitive load for little gains.

Why cannot I do the same with interfaces?

Because one is requiring the implementor to be in your inheritance chain, and thus pick up your defined behavior. The other is not. This is arguably (along with state) the major differences between class and interface inheritance.

Note: if you want to restrict your subtypes, you could do so with an analyzer. I do not htink the language shoudl be motivated to do this. Indeed, i think it fairly heavily violates the spirit of interfaces :)

@CyrusNajmabadi

Because one is requiring the implementor to be in your inheritance chain, and thus pick up your defined behavior. The other is not. This is arguably (along with state) the major differences between class and interface inheritance.

Sorry, I don't think I understand. Both purely abstract class and interface force the inheriting/implementing entity to define the behavior. The purely abstract class forces no behavior on the inheriting entity, the interface with a sealed default implemented member does force behavior on the implementer.

The interface cannot control how it is implemented. The implementor just needs to supply an impl for the interface. That's not true with classes. Classes can define parts of the behavior that the subclass cannot control.

@CyrusNajmabadi I was not speaking about classes in general and interfaces in general. I was speaking about the case of an abstract classes that have only abstract members and that I see no reason why language limits them less than interfaces (code above).
And interfaces with sealed members are not really permissive:

public abstract class NoForcing
{
    public abstract void DoAnythingYouWant();
}

public interface IForced
{
    public sealed void YouMustWriteLine()
        => Console.WriteLine("It's a must.");
}

I would understand if the later (IForced) was forbidden, but it is not, so, unless I completely miss something crucial, it is beyond me why extending properties with setters, overriding return types, and sealing explicit implementations is forbidden for interfaces.

sakno commented

What is difference with #110? Witness structs can do the same.

@sakno First-class support, basically. Implementation details of this will be hidden (and may use witness structs behind the scenes), plus this will not introduce any new concepts into the language.

sakno commented

@orthoxerox , it will

"static abstract" is a new concept and will meaningfully add to the conceptual load of C#.

I don't think that allowing abstracts at the static level will meaningfully impact the conceptual load, especially not compared to something like witness structs, associated types, traits, roles, shapes, or any of the related proposals.

Most C# developers understand how virtual and abstract can be used today. To learn this feature, you basically just have to be told "now it works for static interface methods as well".

The how might be interesting to certain users who care about the lowlevel details, but in practice, you simply tell users this makes the following possible:

public interface IParseable<TSelf>
    where TSelf : IParseable<TSelf>
{
    static abstract TSelf Parse(string s);
}

and I'd expect most users see the value, simplicity, and natural integration with existing concepts of the language.

@MadsTorgersen How are overrides expected to work? Are they allowed?

@tannergooding could you, please, provide an extended example of IParsable<T> usage?

Given that C# does not currently special case TSelf, does not support type inference across multiple statements, and override rules (if allowed for static members) enable returning any derived type (which may not even be present during the build), I suppose real life usage scenarios may get either convoluted or as verbose as the good old object oriented conversion of operations to objects.

The simplest usage is just:

public static T Parse<T>(string s)
    where T : IParseable<T>
{
    return T.Parse(s);
}

As per dotnet/designs#205, the plan is for the libraries to implement these interfaces, as appropriate. Users can then write more kinds of generic algorithms, such as supporting any "parseable" type or any "addable" type, etc.

If you are concerned about the conceptual load of allowing static abstract members, then adding an entirely new construct "shapes" to the mix is even worse. We are very used to classes and interfaces and use of abstract and static; extending those concepts to be more complete is a much simpler and more elegant way to deliver the feature. It feels like shapes is almost an arbitrarily new construct to simplify the work required on the compiler side.

@tannergooding I was looking for a real world-ish example that utilizes non sealed reference types. I understand the feature, but as far as I'm aware, the languages that support this in some way (or support shapes/traits/roles) do not support inheritance.

For example, the following code:

public interface IParseable<TSelf>
    where TSelf : IParseable<TSelf>
{
    static abstract TSelf Parse(string s);
}

public class A : IParseable<A>
{
    public long AValue { get; set; }
    public static A Parse(string s)
        => JsonConvert.DeserializeObject<A>(s);
}

public class B : A
{
    public long BValue { get; set; }
    public static override B Parse(string s)
        => JsonConvert.DeserializeObject<B>(s);
}

public void ReplaceAllWithParsed<T>(IList<T> toReplace, T last, string[] lines)
{
    Debug.Assert(toReplace.Count + 1 == lines.Length);
    for (var i = 0; i < lines.Length; ++i)
    {
        toReplace[i] = T.Parse(line);
    }
    last = T.Parse(lines.Last());
}
var bSerialized = "{\"AValue\":2,\"BValue\":3}";

A a = B.Parse(bSerialized);

ReplaceAllWithParsed(new[] { new B() }, new A(), new[] { bSerialized });

is not exactly easy to follow, in my humble opinion.

@quixoticaxis

Why is that more difficult to follow with static members than it would be with instance members? If anything it's more convoluted with instance members as you would then need a factory interface and implementations to facilitate an instance for that logic.

@HaloFour

Why is that more difficult to follow with static members than it would be with instance members? If anything it's more convoluted with instance members as you would then need a factory interface and implementations to facilitate an instance for that logic.

Because it adds one more resolution path that is decided at compile time. Now we would have the virtual resolution at run time, static virtual resolution at compile time, interface resolution at compile time, also interface resolution at run time, generic arguments' inference at compile time, covariance rules at compile time, and I probably forgot something.

Also, as I've already mentioned, I don't know languages that allow both traits/roles/shapes/static in interfaces and inheritance (in Java/C++ sense like C# does). Please, tell me if you know one, because I'm genuinely interested in understanding how it mixes these concepts and manages to stay concise and expressive.

Even the simple code as

class Number : IZero<Number>
{
    public Number Zero => new Number (0);
}
class ExtendedNumber: Number 
{
   public override ExtendedNumber Zero => new ExtendedNumber(1);
   // please, assume that (Number)ExtendedNumber.Zero != Number.Zero
}

asks for all sorts of trouble.

@quixoticaxis

Because it adds one more resolution path that is decided at compile time.

The compiler can already resolve static members on interfaces. This enables those calls to be virtually dispatched at runtime, which is the practical difference.

Also, as I've already mentioned, I don't know languages that allow both traits/roles/shapes/static in interfaces and inheritance (in Java/C++ sense like C# does). Please, tell me if you know one, because I'm genuinely interested in understanding how it mixes these concepts and manages to stay concise and expressive.

Object Pascal (aka Delphi) and Swift, where static interface/protocol members are frequently used for factories.

Even the simple code as ... asks for all sorts of trouble.

Again, I don't see why the argument is any different for static members than it is for instance members. Allowing a type to declare a generic way of establishing "Zero" or "Identity" is exactly one of the generic numeric use cases that these features intend to enable.

In practice, most of the languages that offer traits/roles/shapes have done so since v1 and so their entire ecosystem is built around that feature. For C#/.NET, these features are being added in 20+ years later and so the features need to work with the existing ecosystem and existing type system.

At the ABI level, something like traits or roles has to be implemented using compile time resolution and specialization (or inlining) or some abstraction like an interface or other dispatch mechanism. This is because you can't codify a method to expect an int32 but pass it a int64 or a Guid and have it still work. These have to be separate methods (or have a common abstraction + dispatch mechanism) because how those values are passed differs (and can differ based on architecture, operating system, language, etc). AOT languages like Rust or C++ will statically determine the concrete T at compile time and generate a specialized method to bind against. There may be some sharing involved, but that sharing ultimately depends on them being ABI compatible or having some dispatch to ensure the correct functionality occurs.

In the case of .NET, we have a strong type system, a largely C compatible ABI, generics, and are JIT compiled. Because we are JIT compiled, you must ensure the relevant information is available to the JIT for it to make the right choices. For something like roles/shapes/traits there is no inherent way to relay that information in IL and so either we need to change IL or we need to pass it along in a compatible fashion. Because the JIT specializes generics over value types and supports dispatch over reference types, you can actually use a combination of generics and interfaces to make something like traits "work". (Define IMyTrait, take M<T>() where T : IMyTrait, create a hidden struct Int32MyTraitSupport : IMyTrait wrapper that does the relevant forwarding, profit).

Static abstracts in interfaces extends the existing support in a natural way and allows interfaces to now express contracts for static members and for those contracts to be implemented by the derived type. It opens the door for something like roles/shapes/traits to be implemented without modifying IL with new instructions/metadata and so will play better with the ecosystem as a whole.

  • Note, I'm not saying IL won't get modified either, that's a discussion for the language/runtime team to have on how they want to implement a given feature. I'm simply explaining how this feature enables you to support something like shapes/roles/traits and how they have to work, realistically, at the ABI level

asks for all sorts of trouble.

Some of these are examples that represent API design which doesn't follow the framework design guidelines and so will be confusing regardless of its a role, shape, trait, instance member, static member, etc

@HaloFour thank you. I don't know Delphi and yeah, I have not thought about Swift.

@tannergooding I'm in no way trying to belittle the effort and I understand that C# is much less agile than the younger languages because it has to carry billions of lines of legacy code. As I mentioned earlier, I'm mostly worrying about the current feature not becoming part of the bigger change, as I personally see "static methods in interfaces accompanied by language supported ability for ad-hoc implementation" as "awesome, bring it on", but "static methods in interfaces alone" seems like a controversial feature (yet again, to me, I neither have the access to the usage data, nor try to generalize). Although I would argue though that the code (at least the snippets I posted) violates the guidelines (because, for example, it's completely normal for derived.Overridden() to be not equal to base.Overriden() (I believe the assumption that the results of the overridden methods should be equal is probably reasonable for low-level or infrastructure code, but it definitely does not hold for the top-level business logic, and, well, Microsoft documentation violates it from the get go even in the article that explains virtual)), I suppose I've come to an understanding of what exactly is being proposed and for what purpose.

Thank you, everyone, for your time and patience.

acaly commented

these features are being added in 20+ years later and so the features need to work with the existing ecosystem and existing type system.

I am also having a little bit of worry on this. The language and runtime is introduced with many new features, while the core library must sustain compatibility. As an example, I personally cannot think of how IEnumerable or IList would evolve (or not) once we have shapes, extension everything, and other fancy new features. Is there a rough plan or some thoughts on this?

Actually there is a real world example: Array class was written obviously before extension method was added. So many methods were added as static methods of Array class. I believe if this can be redesigned they should be added as extension methods. Unfortunately, this is never done or even planned, because of compatibility.