[Proposal] Templates to avoid repetition in class and struct definitions without using inheritance #3758
Replies: 19 comments
-
Source generators could be used for a lot of these cases. Whilst it's always nicer putting the logic into the language, I feel this itch has probably been sufficiently scratched. |
Beta Was this translation helpful? Give feedback.
-
I've just recently started investigating Source generators to add
IEquatable<T> and IComparable<T> to a series of objects and I'm very happy
with the results, so this is definitely an option for this sort of thing.
…On Thu, 30 Jul 2020 at 18:53, Yair Halberstadt ***@***.***> wrote:
Source generators could be used for a lot of these cases. Whilst it's
always nicer putting the logic into the language, I feel this itch has
probably been sufficiently scratched.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<https://github.com/dotnet/csharplang/issues/3758#issuecomment-666562254>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ADIEDQJAA2B6TTX5IUP5CQ3R6GXP5ANCNFSM4POI5WFA>
.
|
Beta Was this translation helpful? Give feedback.
-
Seems really easy, and not a pain at all. |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
I always have to write a project.
why do i need multiple files?
how am i going to find out abotu mixin (using SG style, or using the style you recommend) unless i link to something? How did you think these 'templates to avoid repetition' were going to be distributed? |
Beta Was this translation helpful? Give feedback.
-
Today you find/distribute/discover thsoe by having a project that produces a dll, and distributing that dll. So you're going to have that cost no matter what. the |
Beta Was this translation helpful? Give feedback.
-
I don't know what that means. |
Beta Was this translation helpful? Give feedback.
-
@RUSshy It's not obvious, not at first sight, not at second. It might be obvious to you - but you're clearly heavily invested in the D language and know it deeply. Most of the rest of us are not. Your D example looks very much like a form of multiple inheritance (I'm assuming the D language uses the conventional meaning of the term mixin), something that has repeatedly been rejected by the C# LDM as introducing more pain than value. |
Beta Was this translation helpful? Give feedback.
-
@RUSshy |
Beta Was this translation helpful? Give feedback.
-
If it's easier and simpler, you should be able to explain why that is. |
Beta Was this translation helpful? Give feedback.
-
You seem to be conflating the code of writing a mixin source generator (once-off, upfront) with the cost of consuming it. |
Beta Was this translation helpful? Give feedback.
-
The problem with making it a part of the language is that it is a massive undertaking. You effectively have to make a second language within the language and establish very clear rules as to what executes when and generates what. It's very easily many, many orders of magnitude more involved than source generators. They would have to demonstrate that the value that they can provide justifies that cost. It's not a question of bloat. In fact, it's the exact opposite. Source generators are a very light approach in terms of compiler and tooling effort. Something built into the language can certainly be very powerful, and spinning up a one-off has a much lower barrier to entry. But source generators are also reusable so that cost can be amortized and mixing or templating source generators are likely to start appearing on NuGet, making them just about as easy to use as something built-into the language. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Summary
Structs are not able to define base classes (which definitely makes sense and I don't want that to change). However, in some cases, you have to repeat a lot of code in different structs (and for that matter, also classes. In classes however this is able to be solved for some cases by using inheritance, which is not the case for structs). I propose something I called templates, however if someone has a better idea for a name, I'm open, since template is the name of a very different language feature of C++.
The idea is to put repeating code into the template such that you don't have to copy paste it from type to type (which is also annoying in case you need to change some detail). Templates are not types and they have no effect at runtime. They are only massive syntactical sugar at compile time.
Syntax
Declaration
The template is declared with the keyword
template
. Note the use of the prefix "M" for the identifier. I would propose making this convention so that nobody thinks it is a base class. I use "M" here since "T" is already by convention the prefix for type parameters.Consumption
Templates would be provided like base classes and interfaces. Thus the use of the prefix to avoid confusion.
Example
One use case might be implementing
IEquatable<T>
andIComparable<T>
for a struct which wraps a single value of a type that itself implements these interfaces. I'm going to explain different features of templates used in this example below.Every struct or class that consumes
MEquatableAndComparable<T>
gets a new propertyValue
of typeT
and is equatable and comparable based on this property.Features
self
The example used the keyword
self
.self
is a placeholder for the type that consumes the template. The only information about this type that is has in each case is that defined by the template, so every member defined by the template can be used from the typeself
and fromthis
.Generics
The template is generic, since it defines the type parameter
T
. This has no runtime effect and only allows the template to be more flexible. I'll explain later how templates would be compiled.Interfaces
The template implements interfaces on behalf of the class or struct that uses the template. It has to fully implement the interface. However, if it doesn't know how to implement a certain member of that interface, it can require the consuming class or interface to implement it (more in the next paragraph).
suggested and required
Similar to virtual and abstract members in inheritance relationship, the template can define suggested and required members. Suggested members can be replaced by the consuming class and required members have to provided by it.
An example:
The template wants to implement a clone method:
The consuming class or struct then has to provide a static method called
CreateNewSelf()
. This is able to have any accessibility level, but at least the one defined by the template. This can be any accessibility level, however, interface implementations of course have to be public.When the delevoper of the consuming class or struct thinks they have a better implementation of Clone, they can provide it. Members with the same name (and same signature for methods and indexers) are automatically hidden when redefined in the consuming class or struct.
However, when there is for example a method
void DoWork()
in the template and the consuming class or struct also defines such a method, calls of this method inside the template would still mean it, even though it does not appear in the final type.To use the implementation of the consuming class or struct, members can be decorated with the
suggested
modifier. This way, when any method in the template uses the clone method, it would call the consuming class's or struct'sClone
method.Any member can be required. This also includes constructors. However, constructors are a different topic which I will discuss in detail later, since constructors in structs have to obey some rules that constructors in classes don't.
self constraints
The type
self
is added to the template automatically but behaves a lot like a type parameter. Thus, it can also have constaints. These however work a little differently than constraints on type parameters.This template for example provides the operators
==
,!=
,>
,<
,>=
and<=
for a type which implements the interfaces IEquatable and IComparable. These interfaces have to be already implemented by that type, either by itself or by another template.The constraint also says that self has to be a struct. There is also the class constraint, but it would come in different forms to enable different features.
where self : abstract class
: The template is allowed to define abstract members, since it can only be consumed by an abstract class.where self : base class
: The template is allowed to define virtual and/or protected members, since the consuming class must not be sealed.where self : single class
: The template is allowed to give the class a base class, since it can only be consumed by a class which does not explicitly inherit from any type (because else there would be multi inheritance).where self : concrete class
: This is the opposite of theabstract class
constraint. The purpose is to allow instantiation ofself
using a constructor (more about constructors below).The modifiers can be combined, e.g.
where self : single concrete class
orwhere self : single abstract class
.Along with that, any interface and base class constraint is fine. However,
single class
and a base class constraint must of course not be combined.It is a compile time error to try to make a class or struct consume a template if the constraints do not match.
hidden
hidden
is an access modifier for members defined in the template. Hidden members are not usable by the consuming class, while private members are.Constructors
If there is neither a struct nor a class constraint, the template has to follow the struct rules, since the rules for constructors of structs are stricter than those of classes. If there is a struct constraint, they may only be required and never implemented. That is due to the fact that struct constructors have to initialize every field of their struct. A parameterless constructor is illegal. Only if there is a class constraint, constructors are actually allowed to be implemented. However, only if there is a
concrete class
constraint, the class may call a constructor using thenew
operator (of course it can only call constructors that it defined itself, regardless of whether it implemented them or not).The syntax for constructors:
Multiple templates
You can use multiple templates on one class or struct. When there are multiple members with the same name, the one from the template that is given first is used. When you want to explicitly use a member that is provided in one of the templates, you can use the template name like this:
Whether type parameters have to be repeated in that call (so e.g.
MTemplate<int>.Method
), is up to debate. The problem is that a namespace may not have multiple templates with the same name that differ by the number of type parameters, if you don't have to repeat them as it may cause ambiguity in such a case:Implementation
The implementation would be pretty straight forward. The whole template would be copied into the class or struct that consumes it. Type parameters would be replaced with the type provided by the class or struct. Hidden members and members that are not marked
suggested
but still replaced would get characters into their identifier that are otherwise illegal in identifiers so that they are not directly callable by the consuming class or struct (of course, they can still be called via the template name as stated in the paragraph above, if they are hidden due to an identifier collision). Since templates are only relevant to the consuming class or struct and cannot be used as types, no modification to the runtime would be needed.Why templates?
Coding is about automating tasks. No experienced programmer would write code like this:
Instead, a for loop would be used to get rid of unnecessary repetition, and if a modification to the logic would be needed, only one place needs to change.
Something like that however does not exist yet for classes and structs. As an example, defining operators is very repetitive, and with a template, it would be able to be done once and used for several types.
Examples
MQuantity
MQuantity is a template for a class or struct that defines a quantity (like length, time span, etc.) that should be able to be compared and used with arithmetic operators.
The template:
The full template is here.
As an example, I made three structs that consume this template:
Without the template, all structs would have to define the same operators and interface implementations in the same way, so each struct would have roughly the length of the template now... all repeated code.
MCollection
MCollection is a template that implements the ICollection interface based on a generic list.
The full template is here.
Now, when you want a collection with extra functionality, you can use the template. For example:
Feedback
If someone can think of another edge case that I did not yet cover, let me know. I'm also open to feedback. Thanks ^^
Beta Was this translation helpful? Give feedback.
All reactions