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

API Proposal: Generic System.Math Functions #63732

Closed
alrz opened this issue Jan 13, 2022 · 26 comments
Closed

API Proposal: Generic System.Math Functions #63732

alrz opened this issue Jan 13, 2022 · 26 comments
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Numerics
Milestone

Comments

@alrz
Copy link
Member

alrz commented Jan 13, 2022

Background and motivation

With the introduction of generic operators it's expected to be able to use System.Math functions in a generic manner.

API Proposal

namespace System
{
    public static class Math
    {
        public static T Max<T>(T v1, T v2) where T : IComparisonOperators<T, T>;
        public static T Min<T>(T v1, T v2) where T : IComparisonOperators<T, T>;
        // ...
    }
}

API Usage

T gv1 = e1;
T gv2 = e2;
T max = Math.Max(gv1, gv2);

Alternative Designs

No response

Risks

No response

@alrz alrz added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Jan 13, 2022
@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 Jan 13, 2022
@alrz
Copy link
Member Author

alrz commented Jan 13, 2022

I'm hoping that this is a duplicate but I couldn't find it.

Relates to #63548

@ghost
Copy link

ghost commented Jan 13, 2022

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

Issue Details

Background and motivation

With the introduction of generic operators it's expected to be able to use System.Math functions in a generic manner.

API Proposal

namespace System
{
    public static class Math
    {
        public static T Max<T>(T v1, T v2) where T : IComparisonOperators<T, T>;
        public static T Min<T>(T v1, T v2) where T : IComparisonOperators<T, T>;
        // ...
    }
}

API Usage

T gv1 = e1;
T gv2 = e2;
T max = Math.Max(gv1, gv2);

Alternative Designs

No response

Risks

No response

Author: alrz
Assignees: -
Labels:

api-suggestion, area-System.Numerics, untriaged

Milestone: -

@tannergooding
Copy link
Member

It is a breaking change in overload resolution to add many of these APIs to System.Math and so that is a non-starter.

Take for example: Math.Sqrt(double). Today, if you call this as M(5) it resolves to the only overload available, Math.Sqrt(double). However, if you expose Math.Sqrt<T> this now instead resolves to Math.Sqrt<int>(int) causing a silent change in behavior. This is the reason that MathF originally had to be introduced.

Now, it would be possible to expose Math<T> but there are still issues in that not all operations can or should be exposed for any numeric T. There are many that are restricted to "just floating-point" like types or even beyond that, only IEEE floating-point types.

The path forward that API review had settled on is that since generic math will be exposing all these operations on the T types anyways and since Math/MathF are "special" (non-primitive types typically expose their "math" operators as static methods on the type already; its just the primitive types that have pulled them out "elsewhere") users will just be able to use the relevant interface or concrete type to get the correct behavior.

That is, if you are in a non-generic context, int.Clamp(...) will work. If you are in a generic context, then T will have properly been constrained and T.Clamp(...) will just work. These two scenarios are the only conditions under which some generic Math<T>.Clamp(...) would have been callable anyways (ignoring reflection) so it doesn't miss any functionality.

@alrz
Copy link
Member Author

alrz commented Jan 13, 2022

It is a breaking change in overload resolution to add many of these APIs to System.Math and so that is a non-starter.

I don't know how many APIs could be included here, but I think Min/Max are probably okay? (edit: perhaps in a new static class)

I think generic LINQ operators would go under the same umbrella as well: Sum, Max, Average, to name a few.

@tannergooding
Copy link
Member

tannergooding commented Jan 13, 2022

I don't know how many APIs could be included here, but I think Min/Max are probably okay?

I tried to cover this in the second half of my response. There isn't any benefit to doing this.

If its a generic method then it has to be appropriately constrained. If its appropriately constrained then you have to likewise have either a concrete type that is statically known to meet the constraint or an appropriately constrained T.

In the first case, you can just call the relevant methods int.Min or int.Max. In the latter case, you can do exactly the same T.Min and T.Max.

The first does have a minor exception for the scenario where int explicitly implements the interface (as was done for .NET 6 preview). However, this is likely rare (they will be implicitly implemented in .NET 7) and I'd rather solve the issue by having a nicer language syntax that would be possible with "proper self types", such that INumber<int>.Min and INumber<int>.Max would likewise be possible.

I think generic LINQ operators would go under the same umbrella as well: Sum, Max, Average, to name a few.

APIs that work over "spans", "arrays", or "enumerables" probably shouldn't go on Math. I expect new LINQ methods will go on the System.Linq.Enumerable type and that span/array helpers may just go on MemoryExtensions. These would need their own API proposals and review.

@hez2010
Copy link
Contributor

hez2010 commented Jan 13, 2022

Take for example: Math.Sqrt(double). Today, if you call this as M(5) it resolves to the only overload available, Math.Sqrt(double). However, if you expose Math.Sqrt<T> this now instead resolves to Math.Sqrt<int>(int) causing a silent change in behavior. This is the reason that MathF originally had to be introduced.

In this case, Math.Sqrt<T> can have an IBinaryFloating<T> constraint.

@tannergooding
Copy link
Member

tannergooding commented Jan 13, 2022

In this case, Math.Sqrt can have an IBinaryFloating constraint.

It cannot. Math.Sqrt(5.0f) resolves to Math.Sqrt(double) adding a generic version will make it now resolve to Math.Sqrt<float>(float) resulting in a silent change in precision.

This is made worse by the fact that float is implicitly upcast to double and so the following code may result in double implicit conversion were some overload added that could be resolved to float:

double x = Math.Sqrt(5);

@alrz
Copy link
Member Author

alrz commented Jan 13, 2022

Added an edit to my response. I don't think adding these to the same class is a strict requirement. Following MathF these could be added to another type like MathG or something. However, that wouldn't work for LINQ methods..

@tannergooding
Copy link
Member

This overload resolution problem due to implicit conversion is the reason why MathF exists and why the overloads were not originally added to Math.

However, as I've stated above, I do not believe this to be a problem. There are roughly five contexts under which you want to call these functions:

  • Concrete types where the interface is explicitly implemented
  • Concrete types where the interface is implicitly implemented
  • Generic types with a proper constraint
  • Generic types without a proper constraint
  • Reflection

The first scenario (Concrete types where the interface is explicitly implemented) is a bit complicated. You must define some generic wrapper method to invoke these today and this is the only scenario that providing some Math<T> class in the BCL would help with. I have a self-constraint proposal (dotnet/csharplang#5413), which if implemented as described would provide a nicer alternative as you could just do INumber<int>.Min. Even without language support, it is not expected that "most" types implementing these interfaces will be doing so explicitly and the BCL types will not be implementing these explicitly in .NET 7 (they were only explicit in .NET 6 to help enforce they are preview features and because they were causing problems in some other tooling such as C++/CLI).

The second scenario (Concrete types where the interface is implicitly implemented) is not a problem. You know the concrete type and the method you want is directly accessible, so just call it. int.Min "just works"

The third scenario (Generic types with a proper constraint) is not a problem. As with the previous you already have a proper constraint and so T.Min "just works".

The fourth scenario (Generic types without a proper constraint) is something that is only resolvable via reflection or some new language support that allows you to dynamically meet constraints via runtime checks (that is, some language feature where doing if (T is INumber<T>) for an unconstrained T now means that T.Min is valid within the bounds of the if block.

The fifth scenario (Reflection) is not something we are looking at making simpler. It's an advanced scenario and is already going to have many complications for other reasons. It would be a separate more general purpose feature to make reflection easier to use and not something related to generic math.

@alrz
Copy link
Member Author

alrz commented Jan 13, 2022

T.Min

Do we really need an impl per type for something like Min? I think a single helper constrained to IComparisonOperators should do the job, wherever that's defined.

@tannergooding
Copy link
Member

Given the above, I would need someone here to give a concrete example of where I've missed something or where the normal use-case of T.Min and int.Min are not sufficient.

This does not extend to examples like providing a generic variant of the LINQ Sum method. This I do believe is worth doing and is something that we will be looking at. If someone wants to help by creating an issue covering the APIs where that is useful, that would be beneficial.

Noting that the distinction between LINQ Sum and things like T.Min or that the former is an operation that works over a "collection of scalars" and so there is some additional logic and optimizations that get written on top. Where-as the latter is a direct method that operates on a single T and is already accessible for almost any type implementing the required interface. This means, for the latter, exposing methods provides no additional usability benefits.

@tannergooding
Copy link
Member

tannergooding commented Jan 13, 2022

Do we really need an impl per type for something like Min? I think a single helper constrained to IComparisonOperators should do the job, wherever that's defined.

Yes. Min is not as simple as you might think.

Consider for example floating-point where NaN and -0 exist. The IEEE 754 floating-point spec defines special handling for these scenarios and even goes so far as to define both Min and MinNumber, which differ in their handling of NaN.

Now it may be possible to provide a "default implementation", but that requires default interface method support coming online for static abstracts in interfaces. It also comes with some usability considerations for users wanting to inherit this behavior and expose it directly on their own type.

@tannergooding
Copy link
Member

The same applies to many other methods as well. Things like Abs are "simple" for some cases and "complex" for others.

For a fixed-width two's complement value, such as Int32, Abs(MinValue) is "illegal". For a non-fixed width value such as BigInteger. then its "legal" provided a large enough array can be allocated.

For a one's complement value, all values have a valid Abs(), but it may or may not be relevant to certain inputs like NaN

@Symbai
Copy link

Symbai commented Jan 14, 2022

This overload resolution problem due to implicit conversion is the reason why MathF exists and why the overloads were not originally added to Math.

There was a request to consolidate Math and MathF. Seeing your statement and this request as well, I wonder if it makes sense to introduce a new math class featuring all of these with support of future changes in mind. Then marking System.Math as deprecated, just like WebClient => HttpClient. Which means developer can still use and build with Math, but they get aware of a newer class exist with better support. And they get aware they have to explicit type Max(5.0d) if they want to call the double overload.

@tannergooding
Copy link
Member

I wonder if it makes sense to introduce a new math class featuring all of these with support of future changes in mind.

As per the above (#63732 (comment))

The path forward that API review had settled on is that since generic math will be exposing all these operations on the T types anyways and since Math/MathF are "special" (non-primitive types typically expose their "math" operators as static methods on the type already; its just the primitive types that have pulled them out "elsewhere") users will just be able to use the relevant interface or concrete type to get the correct behavior.

That is, Math is "special" and always has been. When you look at types throughout the ecosystem and even within the BCL, any type which isn't one of the primitive types exposes its "math" APIs as static methods on the type. That is, there isn't VectorMath or BigIntegerMath, etc. You do Vector4.Dot and BigInteger.Max, etc.

Math, from what I recall, was originally introduced because they wanted to keep the primitive types, like Int32, "small". However, with generic math we have to introduce these APIs on the types anyways at which point it makes sense to just make them consistent with the rest of the ecosystem.

The path forward isn't going to be "more Math" classes, its going to be that you consistently access these on the members themselves. So you do int.DivRem, int.Max, int.Min, etc. If you need them in a generic context then you have two options:

  1. You are already writing a generic method with the relevant constraints and so you can just do T.DivRem or T.Max or T.Min
  2. You are writing a generic method without the relevant constraints, in which case you must do the appropriate type checks

The second scenario isn't one that any new math class exposed by the BCL would cover and so exposing a new math class would not provide any additional benefit over what generic math is already providing to the end user.

@hez2010
Copy link
Contributor

hez2010 commented Jan 14, 2022

Now I think both System.Math and System.MathF APIs can be deprecated directly since we already have generic math implemented for all primitive number types. For example, developers can use double.Max() instead of Math.Max(), the type double also makes it explicit when dealing with numbers.

@tannergooding
Copy link
Member

I don't believe we've decided on obsoleting Math or MathF. These APIs are not incorrect and will never be "incorrect" to use. They will always exist and will simply forward to the "correct underlying implementation".

The annotation here is that they may not get new APIs and so if we expose some new API, then Math/MathF are not likely to expose that as well. I think the "best case scenario" is an analyzer that helps point users towards the "new pattern" as part of an informational diagnostic.

@Timo-Weike
Copy link

Now I think both System.Math and System.MathF APIs can be deprecated directly since we already have generic math implemented for all primitive number types. For example, developers can use double.Max() instead of Math.Max(), the type double also makes it explicit when dealing with numbers.

I agree that one should use T.Min and double.Min instead of Math.Min.
But the definition for these static methods is in my opinion misplaced.

They are currently defined in INumber<> but I think they should be defined in IComparisonOperators<>, because you don't need to be able to add or multiply things to be able to sort them.

Also for example the implementation for Math.Min(int,int) only uses the <=, so that is the only dependency for "easy" min and max implementations.

@tannergooding
Copy link
Member

tannergooding commented Feb 10, 2022

Having a comparison that says "x is less than y" is not strictly the same as saying "x is smaller than y".

This is certainly true for most numbers and number like types; but comparisons (and things like IComparable<T>) are also used for sorting and many other scenarios where you only care about ordering. Since ordering (IComparisonOperators) does not necessarily equate to signed magnitude (Min/Max), its not as simple as just moving them down.

@quixoticaxis
Copy link

quixoticaxis commented Feb 10, 2022

Having a comparison that says "x is less than y" is not strictly the same as saying "x is smaller than y".

This is certainly true for most numbers and number like types; but comparisons (and things like IComparable<T>) are also used for sorting and many other scenarios where you only care about ordering. Since ordering (IComparisonOperators) does not necessarily equate to signed magnitude (Min/Max), its not as simple as just moving them down.

Could you elaborate, please, what is "signed magnitude" in the context of your sentence?
Min and max are infimum and supremum that happen to be a part of the set, AFAIK.
I understand that the currently available Max and Min both in Math and in Linq work for multisets by picking any element that is equal to minimum of the set that includes all and only distinct elements of the original superset, but still, I cannot see why comparison is not enough.

@tannergooding
Copy link
Member

tannergooding commented Feb 10, 2022

Take a type where a has a sort order that is less than b and so it is "comparable". In other languages this may be the spaceship operator <=> or may even allow actual comparisons such as "a" < "b" (Python allows this, for example; and I believe IronPython which is Python on .NET does as well).

In such domains a type may implement IComparisonOperators but there is no logical "minimum" here. Just because a is "less than" b does not mean that Min is a valid operation or that Min("a", "b") makes sense.

The ordering of values is not strictly related to their size and therefore whether something is larger or smaller.

@quixoticaxis
Copy link

Take a type where a has a sort order that is less than b and so it is "comparable". In other languages this may be the spaceship operator <=> or may even allow actual comparisons such as "a" < "b" (Python allows this, for example; and I believe IronPython which is Python on .NET does as well).

In such domains a type may implement IComparisonOperators but there is no logical "minimum" here. Just because a is "less than" b does not mean that Min is a valid operation or that Min("a", "b") makes sense.

The ordering of values is not strictly related to their size and therefore whether something is larger or smaller.

I probably don't understand why you imply that max and min mean larger and smaller. They are by math definition are least and greatest, as far as I remember.

@tannergooding
Copy link
Member

I probably don't understand why you imply that max and min mean larger and smaller. They are by math definition are least and greatest, as far as I remember.

Some types make a distinction between sort order (what CompareTo, >, <, >=, and <= do) and Min/Max.

Types may implement the former and not the latter and even when the latter is implemented it is not always defined in terms of the former. IEEE 754 Floating-point types, for example, are another scenario where these operations are not related. -0.0 == +0.0 but Min(-0.0, +0.0) must return -0.0.

@quixoticaxis
Copy link

@tannergooding I see, thank you.

@michaelgsharp michaelgsharp removed the untriaged New issue has not been triaged by the area owner label Feb 14, 2022
@jeffhandley jeffhandley added this to the 7.0.0 milestone Jul 10, 2022
@dakersnar dakersnar modified the milestones: 7.0.0, Future Aug 8, 2022
@tannergooding
Copy link
Member

Going to close this. Users can now trivially provide their own equivalents using Generic Math.

INumberBase<TSelf> provides functions for MinMagnitude and MaxMagnitude while INumber<TSelf> provides functions for Min and Max.

@ghost ghost locked as resolved and limited conversation to collaborators Sep 26, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Numerics
Projects
None yet
Development

No branches or pull requests

9 participants