-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
[Analyzer]: Enforce Curiously Recurring Template Pattern in Generic Math interfaces #69775
Comments
Tagging subscribers to this area: @dotnet/area-meta Issue DetailsSuggested severity: Warning BackgroundWith the introduction of Static abstract members in interfaces and Generic Math Support, a common pattern emerging is to use the Curiously Recurring Template Pattern (CRTP) to enable scenarios where interfaces can declare that a method takes or returns the concrete type that implements the interface. For example: runtime/src/libraries/System.Private.CoreLib/src/System/IParsable.cs Lines 10 to 27 in 6ca8c9b
In this example, public readonly struct DateOnly : IParsable<DateOnly> This allows for developers to write methods that accept an static void ParseAndWrite<T>(string data)
where T : IParsable<T>
{
T parsed = T.Parse(data, null);
Console.WriteLine(parsed);
}
// caller
ParseAndWrite<DateOnly>("2022-05-24");
// prints (in en-US):
// 5/24/2022 IssueAn issue is that nothing is enforcing that a type implementing a curiously recurring template interface fills the generic type with its same type. For example: public readonly struct MyDate : IParsable<DateOnly> The above definition will compile successfully - it is a valid definition from a language perspective. However, ParseAndWrite<MyDate>("2022-05-24"); // error CS0315: The type 'MyDate' cannot be used as type parameter 'T' in the generic type or method 'ParseAndWrite<T>(string)'. There is no boxing conversion from 'MyDate' to 'System.IParsable<MyDate>'. Which is confusing. A similar error Even worse, the ProposalWe add an analyzer that flags types that implement a CRTP interface without passing in the same type as the
When the above rules are met, the analyzer will flag any Type that implements the interface and fills public readonly struct MyDate<TSelf> : IParsable<TSelf>
where TSelf : IParsable<TSelf> The analyzer will not flag the Type. Rejected Alternatives
cc @jeffhandley @tannergooding @buyaa-n
|
@tannergooding, wasn't this part of the plan / an outcome from https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-04-11.md#self-type-stopgap-attribute? (but I thought we were going to do it specifically for the exact types in generic math rather than create something more general that might conflict with a future self type mechanism in the language) |
@stephentoub, could you elaborate on how you think this might conflict? This is restricted to the "exact" scenario that is impacted and equally applies to our own types as to types an end user might also expose. In both cases, it prevents consumers from doing something that may prevent that type from switching to a future |
Because we don't know yet what the future self type feature will be; it hasn't been designed. We're basically inventing our own, via an analyzer. If it's limited to specific interfaces, then it's just a statement around how those interfaces are to be used. Elevating it to the level of a pattern is much broader reaching, especially with a default level of warning, and isn't about our own types: the proposed heuristic is all about how specific language features are used. |
I'm fine with limiting this explicitly to our own interfaces. Users who need the same limitation, such as because they want to have the same future support can just clone our analyzer. |
Tagging subscribers to this area: @dotnet/area-system-numerics Issue DetailsSuggested severity: Warning BackgroundWith the introduction of Static abstract members in interfaces and Generic Math Support, a common pattern emerging is to use the Curiously Recurring Template Pattern (CRTP) to enable scenarios where interfaces can declare that a method takes or returns the concrete type that implements the interface. For example: runtime/src/libraries/System.Private.CoreLib/src/System/IParsable.cs Lines 10 to 27 in 6ca8c9b
In this example, public readonly struct DateOnly : IParsable<DateOnly> This allows for developers to write methods that accept an static void ParseAndWrite<T>(string data)
where T : IParsable<T>
{
T parsed = T.Parse(data, null);
Console.WriteLine(parsed);
}
// caller
ParseAndWrite<DateOnly>("2022-05-24");
// prints (in en-US):
// 5/24/2022 IssueAn issue is that nothing is enforcing that a type implementing a curiously recurring template interface fills the generic type with its same type. For example: public readonly struct MyDate : IParsable<DateOnly> The above definition will compile successfully - it is a valid definition from a language perspective. However, ParseAndWrite<MyDate>("2022-05-24"); // error CS0315: The type 'MyDate' cannot be used as type parameter 'T' in the generic type or method 'ParseAndWrite<T>(string)'. There is no boxing conversion from 'MyDate' to 'System.IParsable<MyDate>'. Which is confusing. A similar error Even worse, the In the future, we hope to adopt a full-fledged self-type mechanism (such as dotnet/csharplang#5413) in the Generic Math interfaces. We would like to enforce that any implementers of the Generic Math interfaces now would not be broken with the adoption of a self-type C# feature in the future. ProposalWe create an analyzer that knows about the generic math interfaces using the CRTP:
The analyzer will flag any Type that implements one of the above interfaces and fills public readonly struct MyDate<TSelf> : IParsable<TSelf>
where TSelf : IParsable<TSelf> The analyzer will not flag the Type. See AlsoRejected Alternatives
When the above rules are met, the analyzer will flag any Type that implements the interface and fills public readonly struct MyDate<TSelf> : IParsable<TSelf>
where TSelf : IParsable<TSelf> The analyzer will not flag the Type. cc @jeffhandley @tannergooding @buyaa-n
|
Thanks, @stephentoub and @tannergooding. I have updated the top proposal to follow the recommended approach. |
Marking as "ready for review". |
I added |
I would put it in |
This isn't just reliability, its a usage requirement around the APIs so we can make a source breaking change in the future. |
Severity: Warning |
@tannergooding this got reviewed and approved. Do we still want it in 7.0? |
I believe yes and the intent is to get this worked on post code complete. |
This analyzer work will happen after the RC1 snap, so this issue will carry past our "zero bugs" goal at the snap. |
Suggested severity: Warning
Suggested category: Usage
Background
With the introduction of Static abstract members in interfaces and Generic Math Support, a common pattern emerging is to use the Curiously Recurring Template Pattern (CRTP) to enable scenarios where interfaces can declare that a method takes or returns the concrete type that implements the interface. For example:
runtime/src/libraries/System.Private.CoreLib/src/System/IParsable.cs
Lines 10 to 27 in 6ca8c9b
In this example,
TSelf
will be filled by the deriving type with its own type:This allows for developers to write methods that accept an
IParsable<T>
, and callParse
on data passed to the method. The method can be written generically without knowing exactly which parsable data type is used to fill theTSelf
.Issue
An issue is that nothing is enforcing that a type implementing a curiously recurring template interface fills the generic type with its same type. For example:
The above definition will compile successfully - it is a valid definition from a language perspective. However,
MyDate
cannot be passed to the aboveParseAndWrite<T>(string data)
method. Trying to useMyDate
in this way will result in a compiler error:Which is confusing. A similar error
CS0311
is raised ifMyDate
is aclass
.Even worse, the
MyDate
type could have been shipped publicly in a library. And only once consumers of this library try to use it asIParsable<DateOnly>
, they get the compiler error. When the real error was thatMyDate
was not defined correctly.In the future, we hope to adopt a full-fledged self-type mechanism (such as dotnet/csharplang#5413) in the Generic Math interfaces. We would like to enforce that any implementers of the Generic Math interfaces now would not be broken with the adoption of a self-type C# feature in the future.
Proposal
We create an analyzer that knows about the generic math interfaces using the CRTP:
IParsable<TSelf>
ISpanParsable<TSelf>
IAdditionOperators<TSelf, TOther, TResult>
IAdditiveIdentity<TSelf, TResult>
IBinaryFloatingPointIeee754<TSelf>
IBinaryInteger<TSelf>
IBinaryNumber<TSelf>
IBitwiseOperators<TSelf, TOther, TResult>
IComparisonOperators<TSelf, TOther>
IDecrementOperators<TSelf>
IDivisionOperators<TSelf, TOther, TResult>
IEqualityOperators<TSelf, TOther>
IExponentialFunctions<TSelf>
IFloatingPointIeee754<TSelf>
IFloatingPoint<TSelf>
IHyperbolicFunctions<TSelf>
IIncrementOperators<TSelf>
ILogarithmicFunctions<TSelf>
IMinMaxValue<TSelf>
IModulusOperators<TSelf, TOther, TResult>
IMultiplicativeIdentity<TSelf, TResult>
IMultiplyOperators<TSelf, TOther, TResult>
INumberBase<TSelf>
INumber<TSelf>
IPowerFunctions<TSelf>
IRootFunctions<TSelf>
IShiftOperators<TSelf, TResult>
ISignedNumber<TSelf>
ISubtractionOperators<TSelf, TOther, TResult>
ITrigonometricFunctions<TSelf>
IUnaryNegationOperators<TSelf, TResult>
IUnaryPlusOperators<TSelf, TResult>
IUnsignedNumber<TSelf>
The analyzer will flag any Type that implements one of the above interfaces and fills
TSelf
with a non-generic Type other than itself. If theTSelf
is filled with another generic Type, such as:The analyzer will not flag the Type.
See Also
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-04-11.md#self-type-stopgap-attribute
Rejected Alternatives
Add an attribute
[SelfTypeAttribute]
that could be applied to the generic parameter of interfaces such asIParsable<[SelfType] TSelf>
. The analyzer wouldn't recognize the recursive pattern itself, but rely on the interface being attributed.Key the analyzer off of the generic type named, literally
TSelf
.Add an analyzer that flags types that implement a CRTP interface without passing in the same type as the
TSelf
parameter. The situation where the analyzer flags code is when a Type implements an interface such that:where T : interface<T>
)static abstract
member (either directly or through inheritance)When the above rules are met, the analyzer will flag any Type that implements the interface and fills
T
with a non-generic Type other than itself. If theT
is filled with another generic Type, such as:The analyzer will not flag the Type.
cc @jeffhandley @tannergooding @buyaa-n
The text was updated successfully, but these errors were encountered: