dotnet / csharplang

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

markm77 commented 3 years ago

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.

masonwheeler commented 3 years ago

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.

masonwheeler commented 3 years ago

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

CyrusNajmabadi commented 3 years ago

without explicitly modifying them to add an interface to them.

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

CyrusNajmabadi commented 3 years ago

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.

masonwheeler commented 3 years ago

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.

CyrusNajmabadi commented 3 years ago

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.

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

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

markm77 commented 3 years ago

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
            }
        }
CyrusNajmabadi commented 3 years ago

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

markm77 commented 3 years ago

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

333fred commented 3 years ago

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

CyrusNajmabadi commented 3 years ago

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

tannergooding commented 3 years ago

Since it has come up a few times...

For anyone interested in discussing this from the libraries side of things (rather than the language), https://github.com/dotnet/designs/pull/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.

333fred commented 3 years ago

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.

markm77 commented 3 years ago

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 3 years ago

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.

YairHalberstadt commented 3 years ago

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

alrz commented 3 years ago

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

alrz commented 3 years ago

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 3 years ago

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.
quixoticaxis commented 3 years ago

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.

HaloFour commented 3 years ago

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

quixoticaxis commented 3 years ago

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

HaloFour commented 3 years ago

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

quixoticaxis commented 3 years ago

@HaloFour thank you for the links.

masonwheeler commented 3 years ago

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.

quixoticaxis commented 3 years ago

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

CyrusNajmabadi commented 3 years ago

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.

quixoticaxis commented 3 years ago

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

CyrusNajmabadi commented 3 years ago

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.

CyrusNajmabadi commented 3 years ago

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

quixoticaxis commented 3 years ago

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

CyrusNajmabadi commented 3 years ago

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.

quixoticaxis commented 3 years ago

@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 3 years ago

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

orthoxerox commented 3 years ago

@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 3 years ago

@orthoxerox , it will

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

tannergooding commented 3 years ago

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.

quixoticaxis commented 3 years ago

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

quixoticaxis commented 3 years ago

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

tannergooding commented 3 years ago

The simplest usage is just:

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

As per https://github.com/dotnet/designs/pull/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.

AdamCoulterOz commented 3 years ago

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.

quixoticaxis commented 3 years ago

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

HaloFour commented 3 years ago

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

quixoticaxis commented 3 years ago

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

HaloFour commented 3 years ago

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

tannergooding commented 3 years ago

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.

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

quixoticaxis commented 3 years ago

@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 3 years ago

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.