csharplang
csharplang copied to clipboard
[Proposal]: Static abstract members in interfaces
Static abstract members in interfaces
- [x] 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 theoperator
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
- https://github.com/dotnet/csharplang/blob/master/meetings/2021/LDM-2021-02-08.md
- https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-06-29.md
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 class
es: abstract
does not have a body, virtual
does.
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.
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.
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 thestring 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()
andpublic 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
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 astatic class
and make it easier instead of allstatic 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.
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 asstatic
, just likestatic 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 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+);
}
@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
returnDateTimeOffset
butDateTimeOffset
-DateTimeOffset
returnTimeSpan
)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.
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.