Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Determine recommended behavior of checked/unchecked operators for DateTime and similar types #67744

Closed
tannergooding opened this issue Apr 8, 2022 · 11 comments
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Numerics design-discussion Ongoing discussion about design without consensus
Milestone

Comments

@tannergooding
Copy link
Member

As per #67714 (comment), there is some an open decision needed around how checked/unchecked operators work for types like DateTime. This likely will play into guidance we give customers for their own similar types as well.

The four basic options are:

  1. Don't implement the generic math interfaces on these types.
  2. Leave the behavior as is. Someone using these types in a generic context might experience exceptions for "overflow" (but not actually OverflowException) even when in unchecked.
  3. Implement the new unchecked operators on these types to not throw. This means someone using these in a generic context will have more consistent behavior regardless of the concrete type being used, but it yields an inconsistency between how the type behaves when using it directly vs generically.
  4. (3) and change the existing behavior of the existing operators to be non-throwing. That's a breaking change in that someone could easily be relying on that behavior to stop "bad things" from happening.

More generically users will have types that expose simply operator + today and that may universally allow (unchecked) or disallow (checked) overflow. Starting with C# 11, users can now also define operator checked + and users will be required to do that if implementing IAdditionOperators.

The user always has the option of preserving the existing public surface and explicitly implementing the interface. This allows a preservation of the current behavior whenever interacting with the concrete type and allows the behavior in an appropriately constrained generic context to work differently.

The user also has the option of implementing the interfaces implicitly and taking a behavioral break for existing dependents. If the behavior was universally allow overflow (unchecked) then the user may see a behavioral break on recompile for code that uses the operator in a checked context as that may now throw for overflow where it didn't previously. If the behavior was universally disallow overflow (checked), then the user may see a behavioral break for already compiled assemblies or on recompile for code that users the operator in an unchecked context as they may not get an overflow where they did previously.

@dotnet-issue-labeler
Copy link

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Apr 8, 2022
@tannergooding
Copy link
Member Author

CC. @stephentoub

@tannergooding tannergooding added design-discussion Ongoing discussion about design without consensus area-System.Numerics and removed untriaged New issue has not been triaged by the area owner labels Apr 8, 2022
@tannergooding tannergooding added this to the 7.0.0 milestone Apr 8, 2022
@ghost
Copy link

ghost commented Apr 8, 2022

Tagging subscribers to this area: @dotnet/area-system-numerics
See info in area-owners.md if you want to be subscribed.

Issue Details

As per #67714 (comment), there is some an open decision needed around how checked/unchecked operators work for types like DateTime. This likely will play into guidance we give customers for their own similar types as well.

The four basic options are:

  1. Don't implement the generic math interfaces on these types.
  2. Leave the behavior as is. Someone using these types in a generic context might experience exceptions for "overflow" (but not actually OverflowException) even when in unchecked.
  3. Implement the new unchecked operators on these types to not throw. This means someone using these in a generic context will have more consistent behavior regardless of the concrete type being used, but it yields an inconsistency between how the type behaves when using it directly vs generically.
  4. (3) and change the existing behavior of the existing operators to be non-throwing. That's a breaking change in that someone could easily be relying on that behavior to stop "bad things" from happening.

More generically users will have types that expose simply operator + today and that may universally allow (unchecked) or disallow (checked) overflow. Starting with C# 11, users can now also define operator checked + and users will be required to do that if implementing IAdditionOperators.

The user always has the option of preserving the existing public surface and explicitly implementing the interface. This allows a preservation of the current behavior whenever interacting with the concrete type and allows the behavior in an appropriately constrained generic context to work differently.

The user also has the option of implementing the interfaces implicitly and taking a behavioral break for existing dependents. If the behavior was universally allow overflow (unchecked) then the user may see a behavioral break on recompile for code that uses the operator in a checked context as that may now throw for overflow where it didn't previously. If the behavior was universally disallow overflow (checked), then the user may see a behavioral break for already compiled assemblies or on recompile for code that users the operator in an unchecked context as they may not get an overflow where they did previously.

Author: tannergooding
Assignees: -
Labels:

design-discussion, area-System.Numerics

Milestone: -

@tannergooding
Copy link
Member Author

tannergooding commented Apr 8, 2022

I'll take this to API review as part of this next round of generic math. Would be good to get some initial thoughts from interested @dotnet/fxdc members beforehand, however.

My own leaning is that 3 is the least breaking and potentially the most useful overall. Utilizing the code from a generic context is an entirely new concept here and so there won't be users relying on an existing behavior. There is no risk of "breaking" existing users and users may even expect a different/overall more consistent behavior generically speaking. There would be an observable difference between DateTime op DateTime vs T op T, but that won't really be "visible" in the same sense unless you're explicitly observing the type later. Overflow likewise doesn't strictly have to be wrapping, saturation is a suitable alternative for some contexts so there are options which may simplify various things.

Users would then be free to decide if 4 (making the exposed public surface area match the generic context behavior) is an additional step worthwhile for the types in question. It would be a behavioral (but non source or binary breaking) change; but that's largely something that only the implementors of a given type can decide if its worthwhile or not.


2 would be the next thing I'd lean towards. It would just clarify that there should be no expectation that unchecked in a generic context "guarantees" OverflowException can't occur. Any type could decide that is always appropriate for their scenario, just like any type could decide x / 0 always throws DivideByZeroException while another could decide to treat it as NaN or Infinity.

@tannergooding tannergooding self-assigned this Apr 8, 2022
@GSPP
Copy link

GSPP commented Apr 17, 2022

This sounds to me like the correct choice can only be made when looking at concrete use cases as examples. This kind of choice is very hard to make theoretically.

So... are there known use cases for generic math on time types? I'm sure there are, and it would inform the decision greatly if plausible situations were looked at. And once the choice is made, it's final for compat reasons.

@GSPP
Copy link

GSPP commented Apr 18, 2022

That said, number 4 (changing behavior to overflow) seems to be very undesirable. It's not just breaking but it makes time bugs more likely and harder to diagnose.

Overflowing a time value intentionally is, I think, a 0% use case that nobody wants ever.

@tannergooding
Copy link
Member Author

This covers more than just DateTime and its likely going to be a bit "case-by-case". But it is something we need to consider and determine if we have recommended guidance around for devs who are going to implement the new generic math interfaces.

As for whether or not overflow is appropriate, it really depends. If you're tracking say TimeOnly, well overflow is often "natural" (11:59 pm -> 12:00am). Sometimes you might want that to wrap and others you might want it to throw.

One could argue the same for DateTime or any other type, but it's something that API review will need to decide. double->decimal is another example that always throws today on overflow, but which reasonably could saturate instead.

@tannergooding tannergooding added blocking Marks issues that we want to fast track in order to unblock other important work api-ready-for-review API is ready for review, it is NOT ready for implementation labels May 10, 2022
@bartonjs
Copy link
Member

bartonjs commented May 12, 2022

Video

The feeling in the room is a mix of (1) and (3).

  • For types that aren't naturally "math" types, like DateTime, don't add any of the math-related interfaces (the ones in System.Numerics shouldn't be added; ISpanParsable isn't mathy, so is OK.)
  • For the types that are, like BigInteger and Decimal, don't change the behavior of the existing members; only in the generic math context will the checked/unchecked differentiation take place.

@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed blocking Marks issues that we want to fast track in order to unblock other important work api-ready-for-review API is ready for review, it is NOT ready for implementation labels May 12, 2022
@tannergooding
Copy link
Member Author

Closing as the relevant APIs have been updated to work as expected.

@ghost ghost locked as resolved and limited conversation to collaborators Jul 12, 2022
@stephentoub
Copy link
Member

@tannergooding, I see we lumped IComparisonOperators into this, e.g. IComparisonOperators was removed from TimeSpan. I don't remember discussing that one in particular, though maybe we did. Is that something we're planning to bring back?

@tannergooding
Copy link
Member Author

We opted for removing all the new operator interfaces from the non-numeric types until more concrete scenarios were presented around their usage and versioning over time.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Numerics design-discussion Ongoing discussion about design without consensus
Projects
None yet
Development

No branches or pull requests

4 participants