Skip to content

Self type in generic

Blokyk edited this page Jul 5, 2021 · 1 revision

Concept

In C#, generic types that need to know the type that implements them have to add a redundant generic parameter, and so every class that implements that interface has to write its type multiple time even though the compiler knows it already. To avoid this problem in lotus, I propose this syntax :

public interface IInterface<this T> 
{ /* interface body */ }

public class SomeClass : IInterface // No need to specify SomeClass again in the inheritance list
{ /* class body */ }

Notice the this keyword in the generic parameter list. This indicates that, by default, the generic parameter T will be assigned to the type that implements the interface. However, just like extension methods in C#, it is still possible to specify a generic argument, just like any other generic type.

Restrictions

This is simply syntax sugar. Therefore, when using that feature, you need to be aware of a few things :

Order of generic parameters

Self-typed generic parameters must be the first in the generic parameter declaration list [Is this really useful ? As long as we take care of conflicts]

Conflicts

You cannot have two types with the same signature modulo a self-typed generic parameter, or the self-typed-ness of a generic parameter. In general, that means that the signature of a type cannot be one above or below that of another containing a self-typed generic parameter. For example, it is a type error to have :

  • Both IInterface and IInterface<this T>, because SomeClass : IInterface could mean
    • SomeClass : IInterface, as in the first type
    • SomeClass : IInterface<SomeClass>, as in the second type
  • Both IInterface<T> and IInterface<this T>, because SomeClass : IInterface<int> could mean
    • SomeClass : IInterface<int>, as in the first type
    • SomeClass : IInterface<int>, as in the second type (with explicit parameter)
  • Both IInterface<T> and IInterface<this T1, T2>, because SomeClass : IInterface<int> could mean
    • SomeClass : IInterface<int>, as in the first type
    • SomeClass : IInterface<SomeClass, int>, as in the second type
  • Both IInterface<T1, T2> and IInterface<this T1, T2>, because SomeClass : IInterface<int, string> could mean
    • SomeClass : IInterface<int, string>, as in the first type
    • SomeClass : IInterface<SomeClass, int, string> as in the second type
    • SomeClass : IInterface<int, string>, as in the second type (with explicit parameter)

And repeat for every variation of IInterface<this T1, T2, ..., Tn> conflicts with IInterface<T1, T2, ..., Tn> and IInterface<T1, T2, ..., Tn-1>.

Constraints

Constraints of the form where T is IInterface (using the same declaration as in the example) will be expanded to where T is IInterface<T>, meaning that only types that implement the interface for their own type will be allowed. If this is not the desired effect, you can always explicitly specify the generic type you desired : where T is IInterface<U> (see conflicts above)