csharplang icon indicating copy to clipboard operation
csharplang copied to clipboard

[Proposal]: Self Constraint

Open tannergooding opened this issue 3 years ago • 22 comments

Self constraint for generic type parameters

  • [x] Proposed
  • [ ] Prototype: Not Started
  • [ ] Implementation: Not Started
  • [ ] Specification: In progress: https://github.com/dotnet/csharplang/blob/main/proposals/self-constraint.md

Design meetings

https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-11-10.md https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md#type-hole-in-static-abstracts

tannergooding avatar Nov 11 '21 18:11 tannergooding

I would say the second option is better, where T : this, and this should be available on classes, too - imagine a similar scenario for an abstract class.

TahirAhmadov avatar Nov 15 '21 14:11 TahirAhmadov

It would be useful to me to have this on classes, too.

jnm2 avatar Nov 15 '21 15:11 jnm2

The best way this could be done is to allow it on classes and make it an associated type to prevent pollution of type parameters at the user side

0x0737 avatar Nov 16 '21 05:11 0x0737

Another thing I just noticed in my own work, imagine a builder class like below:

class Base<TSelf> where TSelf : Base<TSelf>
{
  public TSelf SetOption()
  {
    ...
    return this; // right now this is an error
    return (TSelf)this; // this has to be done
  }
}
// can we make it work like below?
class Base<TSelf> where TSelf : this
{
  public TSelf SetOption()
  {
    ...
    return this; // works
  }
}

TahirAhmadov avatar Nov 18 '21 21:11 TahirAhmadov

Great proposal.

Tiny suggestion: It might be nice if we could reference a self-type in implementing type declarations without having to re-state the type name explicitly. E.g.

interface IBuilder<this TSelf>
{ ... }

class SomeBuilderWithAnAnnoyinglyLongAndComplicatedName : IBuilder<SomeBuilderWithAnAnnoyinglyLongAndComplicatedName>
{ ... }

becomes

class SomeBuilderWithAnAnnoyinglyLongAndComplicatedName : IBuilder<this>
{ ... }

That might improve code readability & brevity (I'd think most class names are >4 characters long) a fair bit.

fabianoliver avatar Dec 09 '21 11:12 fabianoliver

@fabianoliver this is perhaps not the best name choice, as it usually means "current instance" and not "current type".

vladd avatar Dec 09 '21 12:12 vladd

@fabianoliver this is perhaps not the best name choice, as it usually means "current instance" and not "current type".

"This" seemed like a natural fit if the proposal also uses it as a qualifier for the self generic type (i.e. IBuilder<this TSelf>), in which case we have precedent for "this" referring to a type indeed. It would seem like a natural fit in this case (and also would avoid adding extra keywords to the language).

Having said that, personally I wouldn't be too hung about about the specific terminology, another reserved identifier would be fine as well

fabianoliver avatar Dec 09 '21 12:12 fabianoliver

I've been thinking about self-types quite a bit recently, and tl;dr, I don't think this is how we should do them. I think that self-types are better expressed in a framework of associated types (aka existential types #5556, #1328, aka abstract types, aka virtual types) or similar, primarily for the reason that @0x0737 mentions above: not polluting a type with extra type parameters. However, it is possible that we can come up with a notion of "self-constraint" that would apply orthogonally to both type parameters (now) and associated types (if we ever get them), so we may not have to wait for that.

If we did such a self-type/self-constraint feature (on top of current generics and/or future associated types) we would want it to be expressive across the language - in classes as well as interfaces. And we would want examples like @TahirAhmadov's above to work, i.e., this is assignable to any self-type:

class Base<TSelf> where TSelf : this
{
  public TSelf SetOption()
  {
    ...
    return this; // works
  }
}

It's possible that we can come up with such a proposal, but this isn't it. I worry about adopting a notion of self-types that only serves a relative corner scenario (static virtual members, which don't even have a this value). I think we risk "burning" the notion of self-types where we should be saving it for something more valuable and encompassing.

MadsTorgersen avatar Mar 26 '22 03:03 MadsTorgersen

This was discussed in LDM: https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md#type-hole-in-static-abstracts

While we reject this version of the self constraint, that's not to say we're uninterested in self types in general, we just think it should be an existential type, not a constraint on a visible type parameter.

333fred avatar Mar 29 '22 01:03 333fred

An enthusiastic +1 for this concept. Static abstract interfaces is a (somewhat) useful waypoint on the path to TSelf.

C# is too great of a language not to solve CRTP!

craigajohnson avatar Mar 29 '22 21:03 craigajohnson

However, it is possible that we can come up with a notion of "self-constraint" that would apply orthogonally to both type parameters (now) and associated types (if we ever get them), so we may not have to wait for that.

A self-type constraint works for broader scenarios, but it seems to me that it could be introduced implicitly as an associate type with a new type kind. eg trait INumeric { /*TSelf in scope*/ }

That said, there's a few questions for the stopgap attribute proposal (https://github.com/dotnet/csharplang/issues/6000):

  • Should it enforce an ordering (must be the first type parameter?)
  • Should it enforce the type parameter to be invariant?
  • Is it possible to use the new kind for existing types without breaking code? (eg IEquatable<T>)

Mentioned the last point because I think [SelfType] is going to be applied to IEquatable, maybe?

alrz avatar Apr 10 '22 10:04 alrz

[SelfType] attribute is a good idea. But it might be better to have a more dedicated syntax for this in C# grammar, just like nint for [NativeInteger] IntPtr?

Qiu233 avatar Apr 11 '22 18:04 Qiu233

@Qiu233 It's a temporary solution

0x0737 avatar Apr 11 '22 23:04 0x0737

(machine translation) I read an article saying that not everything is inherited from object. Interface is one of them. But when I declare an empty interface type, the IDE still tells me that I can call the toString method. I think this is because the IDE can determine that it inherits from object, even if it doesn't know the content of the implementation class. Then, this feature should be applied to Self Constraint.

Bar<Derive> bar = new Derive();
bar.FooMethod();//This should be valid
Foo foo = bar ;//This should be valid

class Foo
{
	public void FooMethod() { }
}
interface Bar<T> where T :this, Foo
{
}
class Derive :Foo, Bar<Derive>
{
}

zms9110750 avatar May 20 '22 22:05 zms9110750

Yes, that's an excellent example of the type of thing that needs more thought before we have a self or this constraint in the language.

333fred avatar May 20 '22 23:05 333fred

I just came across another place where this would be useful, it would be cool to have things like below:

public delegate void TypedEventHandler<TSender, TArgs>(TSender sender, TArgs args);
abstract class Base<TSelf> where TSelf : this // or "self" or w/e syntax we choose
{
  public event TypedEventHandler<TSelf, EventArgs>? SomeEvent;
}
class Child : Base<Child>
{
}
var child = new Child();
child.SomeEvent += child_SomeEvent;
void child_SomeEvent(Child sender, EventArgs e) { ... }

I also realized we may have a problem:

class GrandChild : Child
{
}
var grandChild = new GrandChild();
grandChild.SomeEvent += grandChild_SomeEvent; // this wouldn't work, would it?
void grandChild_SomeEvent(GrandChild sender, EventArgs e) { ... }

TahirAhmadov avatar Jun 13 '22 00:06 TahirAhmadov

Would there be an issue with doing the Rust thing of having This refer to the type of this in the type system?

Taking TahirAhmadov's example:

class Base<TSelf> where TSelf : Base<TSelf>
{
  public TSelf SetOption()
  {
    ...
    return this; // right now this is an error
    return (TSelf)this; // this has to be done
  }
}
// can we make it work like below?
class Base
{
  public This SetOption()
  {
    ...
    return this; // works
  }
}

*Oops, totally missed where this was suggested above.

jamescarterbellMSFT avatar May 19 '23 18:05 jamescarterbellMSFT

Would there be an issue with doing the Rust thing of having This refer to the type of this in the type system?

If it was going to use a keyword like that why not just use this which is already reserved and couldn't collide with any existing types already called This?

Either way, I don't think the syntax is the problem there. What would that actually compile to in IL? How would it be consumed?

HaloFour avatar May 19 '23 19:05 HaloFour

For years I wish this feature to be available, I hope one day will do. I do always implement all kind of unpleasant workarounds. For me it is one of the most important features that I would like to see it available into C#. Guys, please make this available! It is really valuable feature!

claudiudc avatar Apr 16 '24 07:04 claudiudc