psudo-proposal: Traits/Roles/Shapes/[Your Name Here] + Associated Types + Extension Implementations #9321
Unanswered
Raein-88
asked this question in
Language Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
I'll be using the word
trait
to refer to this proposal.roles
(and the oldershapes
) are both very closely related totraits
but having a distinct name makes discussion easier. Any references to traits in rust will be clarified as such.As this is a "psudo-proposal" there are gaps and not every single question is answered or discussed. Realistically I doubt this will be adopted as-is, but hopefully some of the ideas/solutions here are helpful anyway.
Tldr
Treating trait/role/shape implementations as members instead of as purely interface implementations allows for a lot of bonus functionality.
Overview
A trait is a package of functionality that relies on a contract. Types can implement a trait by providing an implementation for the trait's contract and by doing so gain access to the "package of functionality" (shortening this to just functionality from now on) of the trait.
A good example of something very trait-like in current C# is
IEnumerable<T>
. In the terms of traits the contract ofIEnumerable<T>
is:IEnumerator<T>
GetEnumerator
that returns the type implementingIEnumerator<T>
The functionality (package) of
IEnumerable<T>
is the extension methods defined inSystem.Linq
such asSelect
,Where
, andReverse
(psudo-)Proposal
Definition
A trait is defined much like any other type. They can have the usual access modifiers, be partial, have attributes applied to them, and implement interfaces (More details on that a bit later on). A trait definition will implicitly have a
TSelf
generic parameter (TSelf
is what is usually used for self constraints so I'll stick with that for now). Constraints can be applied toTSelf
as needed.A trait definition also needs the ability to define its contract. Contracts are expressed in terms of members and are very similar to interfaces. A particularly valuable portion of this feature would be contract types, which would be an implementation of associated types.
Functionality members would be the same as defining members in other types. They would be able to access the contract members and use them to implement their features. The
this
keyword would have the typeref TSelf
rather than the type of the trait definition (Need to be careful about potentially allowing reassignment ofthis
for object types, maybe need to tie ability to passthis
by ref to a struct constraint on TSelf similarly toref this
extension methods)Here is a sample of what the definition of a trait could look like.
Most of this so far is fairly in line with what other proposals have presented and shouldn't be particularly surprising. There are a few edge cases particularly around contract types that I am glossing over at the moment and saving for further down.
Implementing a trait
Overview / goals
Trait implementations on types are where this proposal starts to differ from other similar proposals. The primary driver for these differences is that usage of traits supports composition-heavy design of types; in the real world there will be cases where a type is implementing dozens of different traits that may very well have overlapping functionality and and contracts. This feature needs to be able to navigate those types of scenarios cleanly. Some of the major goals I kept in mind when putting this portion together were:
Details
Trait implementations are treated similarly to member definitions. They have an access modifier, an identifier, and are marked as either implicit or explicit with a keyword. The functionality of an explicit trait implementation is only visible through the identifier of that implementation. The functionality of an implicit trait implementation will be visible without specifying the identifier (but an implicit implementation must still have an identifier). Any overlaps between the functionality of different implicit trait implementations can be clarified as needed by providing the identifier.
Implementations of the contract for a trait are given in a scope directly below the trait implementation itself, similarly to the syntax for property accessors or the new extension syntax. Once again, here is a hypothetical code sample. (I'll have a more detailed example further down that shows my idea for the less obvious portions of syntax)
By ensuring all trait implementations for a type (including extensions) have an identifier it becomes possible to navigate even some of the most nightmare fuel scenarios imaginable just by specifying it. Implicit vs explicit also grants direct control over which functionality is visible on your type when you "dot in". Speaking of dotting in...
Using a trait
Functionality from an implicitly implemented trait can be accessed just like any other instance or static members.
Supposing in the code above MyTrait has functionality methods
void DoStuff();
andstatic TSelf Create()
(remember, TSelf is the type implementing the trait, so MyClass here) here are some examples of accessing trait functionality.The
.
operator seems like a natural fit here for explicit trait access. An argument could also be made for using:
or another symbol instead to make it more distinct from fields/properties (MyClass:FooBar.Create();
) so that may be worth discussing.Generics
The trick underneath the way generics can work here will be discussed a bit more in the later sections. There are two problems present that generics will need to resolve. First, when constraints produce an overlap in functionality between two traits on the same type parameter the generic method/type will need to be able to qualify between them, but the generic method does not have the identifier. The second problem is how a generic method will know which implementations to use when a type is passed to it.
The second problem is resolved by making generics be generic over the specific implementation of the trait, not the type that is implementing it. This way any ambiguity will be known to the caller and can be resolved by them switching to explicit implementation access syntax and providing the identifier for the implementation they want to pass as a type parameter. Some example syntax for that:
Note that the syntax for providing multiple explicit trait qualifications on a single type parameter will need to be a bit more verbose. I'm not sure what that would look like exactly but maybe something similar to tuple syntax?
MyFunction<MyType.(Trait1,Trait2)>();
? Regardless, this issue only exists in C# syntax, in metadata this problem won't exist.The first problem also becomes easier to solve with the shift to being generic over specific implementations. We simply can use the type names in the constraints as the identifier because each constraint is being satisfied by a specific implementation individually. Example:
Practical example
Before I take a one way express train to compiler and metadata magic land and lose most everyone reading this I wanted to give a more concrete example of what it might look like if you wanted to define
IEnumerable<T>
as a trait, and then provide an extension implementation of it onSystem.Range
to enumerate the values inside the range.Low level details
A rough overview of the low level side of this is that a trait definition creates an interface that represents its contract portion and a nested generic struct that has a T constrained by the contract interface. At every site where a trait is implemented on a type a nested type would be defined that satisfies that contract interface, and a property would be defined that returns an instance of that nested struct. Because every implementation of a trait within a type has a unique identifier we can sidestep the problems related to compiler generated nested types.
Metadata
All of this (With one exception that I am unsure on) can be lowered to existing metadata. However, the way it is lowered is somewhat complex and it would be worth considering if there are places that a change to metadata would dramatically simplify things. I'll be omitting some details and focusing on the core shapes of the lowered results.
Trait Definition
Trait Implementation
Usage
Generic definition
Trait definition with contract type
Trait implementation with contract type
Generic definition with contract types
Generic contract types (Generic Associated Types)
Edit: I spent some time working through this and it is impossible to lower a generic contract type to the current type system so this would end up being reliant on improvements made there.
For reference here is what the hypothetical lowering would need to be.
It
Runtime
Ideally we would want some level of runtime enforcement of the TSelf type parameters. Additionally we would want some apis that make it realistic for someone to inspect traits with reflection instead of having them go through the compiler generated types.
An idea to help with the generic type explosion
This isn't very fleshed out and there will be a few issues but I felt like mentioning it. Because we have control over these compiler generated types there isn't anything stopping us from emitting two different versions of it. We could kill many of the generic parameters and use virtual calls through interface types to present the same functionality. As long as the jit is aware of the general pattern it could use the less generic versions initially, but then maybe swap over to the generic versions if it decides to do a tier0 rejit of a method.
Beta Was this translation helpful? Give feedback.
All reactions