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

Updating the generic math draft proposal to include changes proposed for .NET 7 #257

Merged
merged 16 commits into from
Apr 2, 2022

Conversation

tannergooding
Copy link
Member

This updates the generic math proposal based on feedback received so far, including from #205, https://devblogs.microsoft.com/dotnet/preview-features-in-net-6-generic-math/, and other sources over social media and from the community.

Comment on lines 674 to 685
public interface INumberBase<TSelf>
: IAdditionOperators<TSelf, TSelf, TSelf>,
IAdditiveIdentity<TSelf, TSelf>,
IComparisonOperators<TSelf, TSelf>, // implies IEqualityOperators<TSelf, TSelf>
IEqualityOperators<TSelf, TSelf>, // implies IEquatable<TSelf>
IMultiplyOperators<TSelf, TSelf>,
ISubtractionOperators<TSelf, TSelf, TSelf>,
IUnaryPlusOperators<TSelf, TSelf>
where TSelf : INumberBase<TSelf>
{
// Alias for AdditiveIdentity
static abstract TSelf Zero { get; }
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For right now, this effectively mirrors the Numerics protocol in Swift. Noting that the missing bits would be:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to call out each of the remaining interfaces/members on INumber and discuss why they exist. People should weigh in on why they shouldn't be on INumberBase (name is a placeholder atm).

First lets cover the ones that make sense to leave off:

  • IComparisonOperators - This one actually makes sense to leave off as it can't be supported by things like Complex
  • IModulusOperators - This one probably makes sense to leave off as it doesn't make a lot of sense for Complex

The ones that probably make sense to have:

  • IDecrementOperators - This is, for many reasons, simply x - 1. It is a general nicety in .NET and something often overlooked
  • IDivisionOperators - Complex numbers support division. What scenarios are people wanting where division isn't possible but something is still a "number"
  • IIncrementOperators - This is, for many reasons, simply x + 1. It is a general nicety in .NET and something often overlooked
  • IMultiplicativeIdentity - Complex numbers have a multiplicative identity and it is 1
  • IUnaryNegationOperators - This is logically Zero - x and INumberBase supports subtraction, zero, and the additive identity

The "controversial" ones:

  • ISpanFormattable - All types support ToString. Supporting culture aware formatting of your data is a "best practice" and helps support globally aware apps. In the worst case, you implement this as => ToString();
  • ISpanParseable - Parsing can get complex in some cases but it is likewise a "best practice" for number types to support something built in. This helps support all kinds of programs, roundtripping of the format strings, and both culture aware/invariant data processing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are also some individual APIs:

  • Abs - Potentially makes sense, would be "inefficient" for types like Complex
  • DimRem - Doesn't make sense without IModulusOperators
  • Max, Min, and Clamp - Doesn't make sense for types without IComparisonOperators
  • Sign - May or may not make sense, it can get complicated when dealing with things like Complex on what the sign is

Also there is: Create, CreateSaturating, and CreateTruncating. As on #205 these exist because there are a lot of scenarios that get cut off when you can't convert from a T to a U. For example, you can't correctly account for or handle overflow or take in user-defined values.

You'd have to specialize on each and every T you want to support (keeping in mind overloading on generic constraints isn't possible) and ultimately throw anyways if you can't get the data you need back out. For something like Swift, you can only create a number from a BinaryInteger and so you can't for example go from float->double or float->int, etc.

Copy link

@vladd vladd Nov 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Abs is actually the same as Magnitude. It makes sense for Complex, but returns a double (which ought to be be an associated type). It can be made to return Complex for complex as well (akin to the function Trunc which returns double and not int for double argument).

I would not include Abs into the general INumber or INumberBase interface, because there are useful number abstractions (like integers modulo n which cannot define Magnitude in a useful way). If we would include Abs into INumber, the users would be unable to represent such structures as an INumber.

DivRem is usually considered only for discrete types, but can be naturally extended to doubles (div = Math.Trunc(a / b); rem = a - b * div) and even to Complex using the same computation (Trunc on a Complex should internally truncate both components, bringing the original Complex to the 1x1 grid). This way we can support IModulusOperators<T, T, T> on Complex too, because modulus is defined through Trunc.
Sign is actually c / |c| = a complex with the same argument and magnitude of 1.

Copy link
Member Author

@tannergooding tannergooding Nov 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Abs is actually the same as Magnitude

In the strictest sense of the definition of "magnitude" vs "absolute value", yes. In terms of how I was referring to them above, no.

Abs in .NET returns the same type as is input and so you have int Abs(int) and it may fail for int.MinValue. Where-as Magnitude in swift returns an associated type which is unsigned for two's complement values and so it would be uint Magnitude { get; }. We don't have associated types and will most likely not be getting them before generic math officially releases so they should be considered not a possible solution here.

I would not include Abs into the general INumber or INumberBase interface, because there are useful number abstractions (like integers modulo n which cannot define Magnitude in a useful way). If we would include Abs into INumber, the users would be unable to represent such structures as an INumber.

.NET integers already default to two's complement modular arithmetic. They wrap around on overflow by default and so int.MaxValue + 1 == int.MinValue. I don't believe its worth cutting off absolute value to support increasingly esoteric use cases and instead such scenarios can decide upon a correct behavior for that scenario instead (e.g. Abs(int.MinValue) will throw; Abs(uint) just returns itself, etc).

Also noting that swift exposes Magnitude on its numeric protocol and so likewise doesn't support a scenario where you can't get the absolute value.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.NET integers already default to two's complement modular arithmetic. They wrap around on overflow by default and so int.MaxValue + 1 == int.MinValue. I don't believe its worth cutting off absolute value to support increasingly esoteric use cases and instead such scenarios can decide upon a correct behavior for that scenario instead (e.g. Abs(int.MinValue) will throw; Abs(uint) just returns itself, etc).

Well, Abs is already pointless not only on esoteric types, but actually on more mundane types like unsigned everything. Okay, esoteric types can just implement fine-grained interfaces and not the generic INumberBase, which solves the problem. But having Abs on unsigned types feels impure.

From the other point of view, if Abs returns the same type as the argument, Complex case is easy (we just return the mathematical magnitude), which is a Complex itself).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But having Abs on unsigned types feels impure.

Logically such an operation makes sense and it is simply a no-op, same as it would be for any signed input that is already positive.

In the context of concrete types, you can manually avoid the operation knowing that its always positive.

In the context of generic math, unless you explicitly constrain to ISignedNumber, then you don't know if the number you have could be negative. You can't overload on generic constraint and duplicating tons of logic just to differ on Abs isn't an ideal setup anyways; nor is explicitly checking if a type could be negative. Instead, just expose the operation, have it do nothing (just like operator +(TSelf value)) and let the JIT or other compiler correctly optimize it.

@@ -667,21 +671,30 @@ The numeric interfaces build upon the base interfaces by defining the core abstr
```csharp
namespace System
{
public interface INumber<TSelf>
public interface INumberBase<TSelf>
Copy link
Member Author

@tannergooding tannergooding Nov 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that one of the prime examples was to support Complex, this would likely be something like (given the current setup, not covering potential changes called out in https://github.com/dotnet/designs/pull/257/files#r755361498):

public interface IComplexNumber<TSelf, TNumber>
    : IDivisionOperators<TSelf, TSelf, TSelf>,
      IMultiplicativeIdentity<TSelf, TSelf>,
      INumberBase<TSelf>,
      ISpanFormattable<TSelf>,
      IUnaryNegationOperators<TSelf, TSelf>
    where TSelf : IComplexNumber<TSelf, TNumber>
    where TNumber : INumber<TNumber>
{
    public static TSelf ImaginaryOne { get; }
    public static TSelf Infinity { get; }
    public static TSelf Nan { get; }
    public static TSelf One { get; }

    public TNumber Imaginary { get; }
    public TNumber Magnitude { get; }
    public TNumber Phase { get; }
    public TNumber Real { get; }

    public static TNumber Abs(TSelf value);
    public static TSelf Conjugate(TSelf value);

    // Acos, Asin, Atan
    // Cos, Sin, Tan
    // Cosh, Sinh, Tanh
    // Exp, Log, Log10. Pow
    // Reciprocal, Sqrt
    // IsFinite, IsInfinity, IsNaN
}

Where struct Complex : IComplexNumber<Complex, double>

Copy link
Member Author

@tannergooding tannergooding Nov 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't cover things like IAdditionOperators<TSelf, TNumber, TSelf>

It doesn't cover splitting out functions like Sin/Cos/Tan into some interface that can be shared between IComplexNumber and IFloatingPoint

It doesn't cover creation or conversion APIs.

Copy link

@vladd vladd Nov 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IAdditionOperators<TSelf, TSelf, TNumber>

Did you mean IAdditionOperators<TSelf, TNumber, TSelf>? Perhaps it would be better to just declare an implicit conversion from TNumber to TSelf in IComplexNumber.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean IAdditionOperators<TSelf, TNumber, TSelf>

Yes. Fixed above.

@tannergooding
Copy link
Member Author

I tagged a few of the most active participants on #205 asking them to give additional feedback here.

@vladd
Copy link

vladd commented Nov 24, 2021

I actually don't like the name INumberBase because, well, it just sounds like "some base interface for number". I'd propose renaming INumberBase to just INumber, and INumber to ICanonicalNumber.

And I would add increment and decrement only to IInteger (if we are going to have one), since I suspect that increment/decrement is useful only for integers.

@tannergooding
Copy link
Member Author

I actually don't like the name INumberBase because, well, it just sounds like "some base interface for number". I'd propose renaming INumberBase to just INumber, and INumber to ICanonicalNumber.

As covered in the #205, this is effectively a non-starter. What is currently INumber is going to be the first thing the majority of developers are going to look for and want to use. INumberBase (or some alternative name) will be a thing that only a small subset of developers doing very specific and domain specific things, like dealing with Complex, will need or want to interact with.

And I would add increment and decrement only to IInteger (if we are going to have one), since I suspect that increment/decrement is useful only for integers.

C#, F#, and other .NET languages already expose and support increment/decrement on the primitive number types. Such operators are already fairly standard as is the behavior they have.

@maxild
Copy link

maxild commented Dec 29, 2021

Why does IFloatingPoint<TSelf> have an IEEE 754 API (half, single/float, double, aka base 2 floating point types of different sizes/precisions encoded according to IEEE 754), and IBinaryFloatingPoint<TSelf> is only bringing in IBinaryNumber<TSelf> (bitwise operations)? This effectively will make other floating point types such as System.Decimal (base 10 floating point type) only have support for ISignedNumber<TSelf>? Why make decimal (and other non-BCL fractional/non-integer number types) miss out of static abstract methods for such basic functions such as ceil, floor, truncate, round (i know they are named differently in BCL)? You need to fix this oversight before shipping the number abstractions from .NET 6 (possibly in .NET 7).

See also dotnet/runtime#62293

Also IBinaryFloatingPoint<TSelf> does derive Log2 from both IBinaryNumber<TSelf> and IFloatingPoint<TSelf>?

On a final note. Is the mixing of textual (parsing, printing text representations) and math/arithmetic concerns in INumber<TSelf> because of a constraint wrt perf and number of interfaces (load time of CoreClr), or is it because users should not be forced to constrain the generic type to more than one interface (or maybe other concern)? I know it has been discussed before, just couldn't figure out why this design is chosen? The mental jump from fine grained IXXXXOperators to INumber just seem huge, like it was designed by (and for) two different groups.

On a final note.....

After giving this some more thought my main concern with the proposed design is not so much that numbers should implement ISpanParseable and ISpanFormattable. I can't think of any reason not to. I would even call it a flaw, if a numeric type in .NET didn't implement those two interfaces, because every number should be represented in text (shown to user and read from ReadonlySpan<char>).

My main concern with the current proposal is that the API is divided between very fine grained IXXXXOperators set of operators and too coarse INumber hierarchy, where types like rational ("arbitrary precision fraction", "continued fraction" or what not), "non-IEEE floating point types" (decimal is one example) are all hard to fit in. There has also been previous discussion about "Complex" is hard to fit in.

Some questions I have when seeing the many "IXXXXOperators" interfaces:

  1. What numeric type would implement addition and not implement subtraction (and vice versa)?
  2. What numeric type would implement addition and subtraction, and not implement multiplication?

NOTE: Division is a bit harder, because of "division with truncation toward zero" (integer division in C) and "division with truncation toward negative infinity (division by flooring), but I am honestly in doubt if .NET has both definitions avaliable today. Anyway important in applications of Euclid’s theorem and other number theory fundamentals.

Many more such questions are easy to come up with, because the current proposal has on one hand a set of (not very usable) independent interfaces (often with only a single operator), and another set of number interfaces, that seem to target only a subset of possible numeric types, because there a too many operators (functions) in each of the too few abstractions.

It would be better to come up with a design in the middle between the "too many" IXXXXOperators<T> interfaces and "too few" INumber<T> interfaces that could actually model all the possible numeric types, and be usable for library authors?

@tannergooding
Copy link
Member Author

On a final note. Is the mixing of textual (parsing, printing text representations) and math/arithmetic concerns in INumber because of a constraint wrt perf and number of interfaces (load time of CoreClr), or is it because users should not be forced to constrain the generic type to more than one interface (or maybe other concern)? I know it has been discussed before, just couldn't figure out why this design is chosen? The mental jump from fine grained IXXXXOperators to INumber just seem huge, like it was designed by (and for) two different groups.

Conversion to/from strings and number types is one of the most core scenarios you can do in computer programming. It is one of the first things new developers learn, it is one of the primary ways you interact with users or even external data, and it is something that almost every language that exposes numeric like interfaces/protocols supports.

What numeric type would implement addition and not implement subtraction (and vice versa)?
What numeric type would implement addition and subtraction, and not implement multiplication?

The operator interfaces may support many things that are outside the realm of numbers, including things like DateTime or TimeSpan. In cases like there, there are scenarios where certain operations are legal, but not the inverse.

Even within the realm of number like types; there are cases like Vectors or Matrices where Matrix * Scalar is legal; but where Matrix + Scalar is illegal; or vice-versa

@tannergooding
Copy link
Member Author

This effectively will make other floating point types such as System.Decimal (base 10 floating point type) only have support for ISignedNumber? Why make decimal (and other non-BCL fractional/non-integer number types) miss out of static abstract methods for such basic functions such as ceil, floor, truncate, round

I've been on vacation for the past month and will be working on addressing some of these issues and updating the proposals accordingly. Part of this came down to time limitations in getting the .NET 6 preview out.

@JeffreySax
Copy link

JeffreySax commented Jan 4, 2022

I fully understand that the target audience is the general public. Besides, the type system isn't rich enough to capture all the nuances of algebraic structures even if we wanted to. (Example: matrix multiplication with rectangular matrices.)

The super fine-grained interfaces seem at odds with that idea. Who in the general public can name a kind of number that does not have a multiplicative identity? I think it's reasonable to assume that anyone who uses such a type should be aware of this fact. So I don't think it's necessary to have a separate IMultiplicativeIdentity. Let it throw NotSupportedException if need be.

Base interfaces

Addition

Let's start with the group (no pun intended) of addition and related operations. There are really two kinds of numbers:

  • "Normal" numbers that have a zero element.
  • Scales, like time, temperature, [U]IntPtr...

Normal numbers have a 0 and can be added, subtracted, negated between themselves: 0, T+T, T-T, -T.
Scales usually have an associated "difference" type (D) that is a normal number, and defines: T+D,D+T,T-D,T-T=D.
Then there are mixed operations (for example: int + BigInteger): T1+T2, T2+T1, T1-T2, T2-T1.

These 3 (types of) sets are mutually exclusive. To me it would make much more sense to group the operations in this way than to have the current 4 interfaces grouped by operation.

Also, I don't know of any examples where addition is not commutative.

More generally, I can't think of any non-contrived examples where you have 3 different types (T+U->V). Even if there are, would you need to create an abstraction that can generically use addition in this way?

Multiplication

A similar argument can be made for multiplication but it's trickier because there are many more subtleties with division. TimeSpan can be seen as a multiplicative scale with double as the quotient type.

Two other points of note:

  • I already mentioned that I don't believe a separate IMultiplicativeIdentity interface is necessary.
  • Non-commutativity does not imply that U*T is not defined if T*U is defined. I can't name an example where both operations wouldn't be acceptable.

Checked operations

Checked context is only relevant for integer operations. Non-integer types shouldn't be burdened with having to implement them.

Generic method type parameters

In several places, an interface contains a method that includes a generic type parameter. Examples are: conversions, bit shift operations... This creates an expectation that (for example) general conversions are supported. It may be better to define conversions separately and include a minimal set in the numeric interfaces (say IConvertFrom<Int32> for integer types).

Numeric interfaces

Rather than try to squeeze everything into an INumberBase interface, wouldn't it make more sense to have several distinct interfaces?

  • INumber: for actual numbers: integers, reals, rationals, complex numbers...
  • IAdditiveScale: for things like DateTime. Includes comparisons.
  • IMultiplicativeScale for things like TImeSpan. Includes comparisons.
  • Anything else you can compose it yourself.
    In addition:
  • INumber can be: IReal (including comparisons, abs, sign...) or IComplex (conjugate, i...)
  • IReal can be: IInteger (bitwise, checked operations), INonInteger (round/floor/ceiling/...)
  • IFloatingPoint is stand-alone and defines IEEE-754 stuff, elementary functions, etc.
  • INonInteger can be IRealFloatingPoint (add IFloatingPoint)
  • IComplex can be IComplexFloatingPoint (add IFloatingPoint)

Capability queries

I think it might be useful to add a method to query whether a type supports an operation. For example: CanCreateFrom<T>().

@tannergooding
Copy link
Member Author

Let's start with the group (no pun intended) of addition and related operations. There are really two kinds of numbers:

Most of the "operator" interfaces are there to support more than just the generic math scenario. There are hundreds of thousands of types across all .NET code in existence and the actual contract for these operators is a generic one that can accept all kinds of designs and shapes that don't fit the normal model for mathematics, numerics, or even recommended practices.

Even looking in the BCL, there are concepts like DateTime and TimeSpan which support several of these operations, but which don't support all operations. As you start looking at things like Complex or Vector/Matrix/Plane/Quaternions and other types you start encountering more examples where one API existing does not guarantee a counterpart or related API to also exist.

Also, I don't know of any examples where addition is not commutative.

Concatenation (such as for strings) uses the addition operator and is non-commutative "Cat" + "Dog" and "Dog" + "Cat" produce different strings. Rotations are typically non-commutative as well.

Non-commutativity does not imply that UT is not defined if TU is defined. I can't name an example where both operations wouldn't be acceptable.

While many libraries expose both Matrix * Vector and Vector * Matrix are very different operations and differ in whether they produce a Matrix or Vector type (and further differ based on whether they are row-major or column-major).

Checked context is only relevant for integer operations. Non-integer types shouldn't be burdened with having to implement them.

There is no trivial way to restrict these to just integer types as we can't know who will implement the interfaces ahead of time. Likewise, there are a plethora of scenarios where checked arithmetic is applicable to other types, including floating-point. While .NET doesn't expose such support today, the IEEE 754 spec does have well defined behavior and exception reporting (effectively checked arithmetic) for overflow, underflow, and inexact results. The checked versions of operators could enable a software version of this functionality or could be implemented via an IDE quick-fix to simply match the unchecked behavior (much as several other interfaces have quick fixes to provide the common functionality).

In several places, an interface contains a method that includes a generic type parameter. Examples are: conversions, bit shift operations... This creates an expectation that (for example) general conversions are supported. It may be better to define conversions separately and include a minimal set in the numeric interfaces (say IConvertFrom for integer types).

There is no good way to provide or expose this. Functionally speaking, you will have an arbitrary unknown type and need to convert it to another type (possibly known and possibly unknown) in many scenarios.

We have real world limitations on the number of interfaces we can expose on the primitive types. There are possibly some better ways this could be exposed; and I am in the process of investigating them, but I expect this will ultimately be a pain point for different users in different ways. Even just considering the primitive types, there are ~14 types that can all convert between each other (10 integer types, 3 floating-point types, and decimal). There then also exists things like BigInteger which can also generally convert from all of them. The "simplest" thing would be to have some interface that says "I can convert to/from the primitive integer types", another covering the "primitive floating-point types", and then a general purpose IConvertibleFrom/IConvertibleTo to cover the other scenarios; but there are still pain points. These pain points include things such as the number of primitive types that would need to be supported and even extend to considerations such as: "what do I do if both TFrom.ConvertTo and TTo.ConvertFrom both exist?" or "how do I declare an API that must be able to convert between T and U where they are independent libraries with no knowledge of eachother.

When all of those are considered; the initial preview decided on generic TryConvert APIs being the "simplest". It's still complex, but it avoids most of the problems around "what constraints do I use", allows code to generically support conversion via getting the raw bits or components for types like integers or floating-point values (which allows conversions between libraries that don't understand eachother).

It's not the only solution, but it is the one that we landed on for the .NET 6 preview and I will continue working on figuring out the best solution here before we finalize the feature.

@JeffreySax
Copy link

@vladd:

I actually don't like the name INumberBase

@tannergooding:

INumberBase (or some alternative name)

What about IOperand?

@tannergooding
Copy link
Member Author

What about IOperand?

There is always some going to be happy about some name we choose. IOperand isn't going to be a good option because operand has an existing semantic to many programming languages, including in the context of CIL and .NET. The word operand in computer programming, while coming from the mathematical definition, generally means something similar to argument or parameter and so using it to mean the mathematical name is going to lead to downstream confusion and pain points.

@maxild
Copy link

maxild commented Jan 4, 2022

I've been on vacation for the past month and will be working on addressing some of these issues and updating the proposals accordingly. Part of this came down to time limitations in getting the .NET 6 preview out.

Fair enough, and happy new year. I am probably repeating myself below. No pun intended. Just want to help make this feature work the best. I have a lot of duplication wrt computations using both double and decimal (primarily) because it is not always clear when it is best to use base2/hardware-based computation and when to use base10/software-based computations in my application domain (money, and other "things" that cannot be rounded outside my control).

Conversion to/from strings and number types is one of the most core scenarios you can do in computer programming.

I am perfectly fine with ISpanParseable and ISpanFormattable in any "number" type. That was just a "beginner" impulse
in my brain, I had to overcome. Numbers are inherently tied to being 'shown/written' to Span<char> and being 'parsed/read'
from ReadOnlySpan<char>.

Concatenation (such as for strings) uses the addition operator and is non-commutative "Cat" + "Dog" and "Dog" + "Cat" produce different strings. Rotations are typically non-commutative as well.

(Strings, +) is not a group (it is a monoid). You cannot undo concatenation (+ has no inverse operation). Therefore any Ring and/or Field like laws/axioms in relation to String is outside the scope of generic math I hope). Therefore strings should not be part of any discussion of generic math ("numbers").

The operator interfaces may support many things that are outside the realm of numbers, including things like DateTime or TimeSpan. In cases like there, there are scenarios where certain operations are legal, but not the inverse.

You mention Calendar related things (date and time). The calender is highly(!) irregular (you can add/subtract days, but nobody wants to work with day offsets) and culture dependent (Gregorian calendar is not used everywhere, Daylight saving time is different around the globe). So in my opinion date (and probably also time = day mod 24h) is better left out.

Is DateTime, DateTimeOffset, DateOnly part of the generic math proposal?

So what exactly is "APIs to support generic math"?

what exactly are the types behind "number"? Are we talking about numeric types only?

Are the IAdditionOperators<TSomeNumberType> interface "Parametricity" done differently. How can I write a generic algorithm using it, if I don't know if IInverseAdditionOperators<TSomeNumberType> == ISubtractionOperators<TSomeNumberType> exists. Or if I don't know if IAdditiveIdentity<TSomeNumberType> exists. Do I have to constrain with all 3 interfaces to form something that work as expected from abstract/modern algebra?

Do I have to stay away from the very generic IXXXXOperators<T, U, V> interfaces and use INumber<T> (with single type parameter, aka closure)?

Are we going to have more/better number abstractions than INumber<TSelf>, IBinaryNumber<TSelf>, ISignedNumber<TSelf>, IUnsignedNumber<TSelf>, IFloatingPoint<TSelf> and IBinaryFloatingPoint<TSelf>?

(This comment #257 (comment) are also touching upon the lack of number interfaces)

This is something I cannot wrap my head around at the moment.

By having an "atomic/disconnected" bag of "small" interfaces, because we are considering too many "things" (strings, date and time, integers, reals, complex numbers, vectors, matrices, ...., maybe more,....., please stop me:-)), then it is extremely difficult to find "Math" and "Algebra" in the "mess". CoreLib types such System.Decimal are lacking APIs (ceil, floor, truncate, round), and DivRem (division with remainder) is implemented in a weird/wrong way for non-integral types (IEEE floats and decimal) that does not meet my expectations that the (quotient, remainder)-pair satisfies x = q*y + r, when x is the dividend and y is the divisor (this can perfectly well be generalized to fractional numbers, but the current implementation forgets to truncate the quotient, probably because it is only supported in the Math class for integers in .NET 6).

After playing with the new interfaces for a while, I could not substitute small portions of my existing generic math API's, because the algorithms I was trying to convert over was based on Euclidean division (division by truncating, actually both Truncation-division -- C# % mod operator on double and decimal is consistent with socalled T-division). And I supported using decimal, Fraction (custom exact type), double in those generic algorithms.

In the first proposal there was i BIG graph/tree of interfaces and concrete "number" types. It was a bit hard to read. Maybe a table with the BCL types that will get "1. class" support (what ever that is) for generic math APIs would be appropriate. Then it would be easier for me to grasp how big a problem we are trying to solve with this proposal. At the moment it is not clear to me what exactly we are trying to solve. It seems a bit like "1. class support for "system programming" (C-like) integers and "IEEE floats", and "second class support" for decimal. I do not work with polynomials (complex numbers) or SIMD instruction vectors so I cannot evaluate support in that space, but I guess the IEEE float interfaces are working great for SIMD abtractions.

@tannergooding
Copy link
Member Author

(Strings, +) is not a group (it is a monoid). You cannot undo concatenation (+ has no inverse operation). Therefore any Ring and/or Field like laws/axioms in relation to String is outside the scope of generic math I hope). Therefore strings should not be part of any discussion of generic math ("numbers").

Static abstracts in interfaces extends to more than just numbers as do the applications for many of the operator interfaces. Likewise, many concepts from mathematics (like rings and fields) do not have clear 1-to-1 mappings in programming.

INumber itself applies to "just numbers". IAdditionOperators and the other "operator interfaces" has many potential uses outside of this and so while many of the concepts are being driven from the aspect of "math", they also extend to programming in general and all the quirks and nuances that can come about from that.

@maxild
Copy link

maxild commented Jan 4, 2022

Scales usually have an associated "difference" type (D) that is a normal number, and defines: T+D,D+T,T-D,T-T=D.
Then there are mixed operations (for example: int + BigInteger): T1+T2, T2+T1, T1-T2, T2-T1.

I would argue that the binary:T x T -> T and unary:T->T case (single type) should be the main focus. This have to be done right. I would like to see a table with all other cases with 2 (or 3) types that need to be supported. Again, a summary of the overall feature listing all the supported "numeric" BCL types.

@maxild
Copy link

maxild commented Jan 4, 2022

Static abstracts in interfaces extends to more than just numbers as do the applications for many of the operator interfaces.

Of course this extends to everything. But at the Library level, doesn't it complicate this feature a lot, to focus broadly on Polymorphism using (stateless, static) functions in general. But for "generic math" besides Parse and ToString/TryFormat (as agreed) shouldn't the feature focus on math/numbers?

There is danger in treating IXXXXXOperators<T, U, V> as Func<T, U, V> (I am not saying you do that), but I am only trying to demonstrate that a (type parametric) polymorphic function always need to constrain the type to some expectations (aka axioms, laws) and this is best done using the type system, if possible.

INumber itself applies to "just numbers".

I have focused on "numbers" and "math" above.

@tannergooding
Copy link
Member Author

Are the IAdditionOperators interface "Parametricity" done differently. How can I write a generic algorithm using it, if I don't know if IInverseAdditionOperators == ISubtractionOperators exists. Or if I don't know if IAdditiveIdentity exists. Do I have to constrain with all 3 interfaces to form something that work as expected from abstract/modern algebra?

Do I have to stay away from the very generic IXXXXOperators<T, U, V> interfaces and use INumber (with single type parameter, aka closure)?

Most users will be expected to use more constrained interfaces, such as INumber or IBinaryInteger/IBinaryFloatingPoint, etc. It is not possible, nor is it a goal of the operator interfaces, to try and expose concepts such as commutativity, distributivity, inverse operations, etc.

Higher level interfaces, such as INumber can themselves define more strict contracts, such as that + and - are inverse operations.

Are we going to have more/better number abstractions than INumber, IBinaryNumber, ISignedNumber, IUnsignedNumber, IFloatingPoint and IBinaryFloatingPoint?

There may be a slightly different split, but it is not likely to grow more significantly. The BCL will provide the "core" interfaces allowing other users to build their own types and cover common algorithms and type interchange scenarios. Users interested in more specialized scenarios can then define their own interfaces built on top of the interfaces we provide or even their own hierarchies.

By having an "atomic/disconnected" bag of "small" interfaces, because we are considering too many "things" (strings, date and time, integers, reals, complex numbers, vectors, matrices, ...., maybe more,....., please stop me:-)), then it is extremely difficult to find "Math" and "Algebra" in the "mess".

I don't agree. The BCL provides many primitive building blocks which are only useful when combined with the other building blocks. Many of the operator and numeric interfaces being exposed as part of this proposal are in the same vein. They are simply the tools/building blocks on which more complex and useful types can be built.

CoreLib types such System.Decimal are lacking APIs (ceil, floor, truncate, round)

Decimal is a bit of an odd type in the first place in that it is a non-standardized type and doesn't fit in well with many of the concepts you might want to support for a floating-point type. Decimal is really a Currency type and so doesn't support concepts that allow it to trivially support many operations that float and double support.

Ceil, Floor, Truncate, and Round are exceptions and this was intentionally skipped for the .NET 6 preview due to time constraints.

and DivRem (division with remainder) is implemented in a weird/wrong way for non-integral types (IEEE floats and decimal) that does not meet my expectations that the (quotient, remainder)-pair satisfies x = q*y + r, when x is the dividend and y is the divisor (this can perfectly well be generalized to fractional numbers, but the current implementation forgets to truncate the quotient, probably because it is only supported in the Math class for integers in .NET 6).

We certainly could update it to meet that contract and to ensure that DivRem the integer division and remainder result or we could move DivRem to be integer specific, both are valid options here.

In the first proposal there was i BIG graph/tree of interfaces and concrete "number" types. It was a bit hard to read. Maybe a table with the BCL types that will get "1. class" support (what ever that is) for generic math APIs would be appropriate. Then it would be easier for me to grasp how big a problem we are trying to solve with this proposal. At the moment it is not clear to me what exactly we are trying to solve. It seems a bit like "1. class support for "system programming" (C-like) integers and "IEEE floats", and "second class support" for decimal. I do not work with polynomials (complex numbers) or SIMD instruction vectors so I cannot evaluate support in that space, but I guess the IEEE float interfaces are working great for SIMD abtractions.

The actual markdown already contains this independent of the graph. However, just considering what the BCL will do/expose is not sufficient. These are primitive building blocks that any .NET library could use for their own scenarios and which could fulfill a completely different contract from the one that another user expects.

That is one reason why there are the "operator" interfaces and the "numeric" interfaces. Both play into generic math, but the former can be extended well beyond numerics.

@JeffreySax
Copy link

@tannergooding Thank you for the clarifications.

Most of the "operator" interfaces are there to support more than just the generic math scenario. There are hundreds of thousands of types across all .NET code in existence and the actual contract for these operators is a generic one that can accept all kinds of designs and shapes that don't fit the normal model for mathematics, numerics, or even recommended practices.

What is the added value of the base interfaces (IXxxOperators)? What's the point?

You give two motivating examples. Both involve numbers. (As an aside, I think it would be instructive to write out the second example in terms of generic math instead of relying on LINQ's generic Sum.)

It's true that operators can be used for lots of other stuff besides numbers, but that doesn't justify why there should be such fine-grained interfaces. Why isn't it good enough that you can define your own interfaces that can declare operator contracts?

When you define an interface, that must mean something. IComparable has one method, but that one method allows you to define an order for your type, sort collections... It's a very general concept with a very clear purpose that means the same thing everywhere.

The same is not true for operators. Does it make sense to define a single interface that covers both generic math operations and operations "that don't fit the normal model for mathematics, numerics, or even recommended practices"?

In addition, for generic operands (like Vector<T>), I find there is much less of a need for generic interfaces on that level because you usually control the entire type hierarchy and you can define operations in terms of operations on elements. See, for example, Silk.NET, which has several variations of generic vector/matrix products. The types in System.Numerics.Vectors only define the standard operators (+-*/[=!]=).

@maxild
Copy link

maxild commented Jan 4, 2022

Decimal is a bit of an odd type in the first place in that it is a non-standardized type and doesn't fit in well with many of the concepts you might want to support for a floating-point type. Decimal is really a Currency type and so doesn't support concepts that allow it to trivially support many operations that float and double support.

I am not sure, but I think .NET 1.0 brought Decimal over from oleautomation (VB 6, VBA, COM). And IEEE 754 (2008) created decimal types, based on the IEEE idioms of normal and subnormal numbers (denormalized numbers around zero and NaN, +inf and -inf). @tannergooding You know more about IEEE 1985, 2008 and 2019 than I do, so please correct me if am wrong. Both IEEE decimal and .NET decimal are "Currency" type (base 10).

Since it is unlikely that the BCL will support IEEE 754 decimals, I urge you to consider giving System.Decimal (that does not have the "IEEE idioms") a place in the INumber hierarchy (probably a common base interface of IFloatingPoint, because both are fractional numbers). This was what dotnet/runtime#62293 was all about (I don't think I stated that very clearly, I am afraid)

@tannergooding
Copy link
Member Author

What is the added value of the base interfaces (IXxxOperators)? What's the point?

They form the building blocks on which other interfaces and hierarchies can be built.

There are many, many, many types where addition get used ranging from strings to quaternions to dates/times to events and lists. Some of these are C# specific and some are specific to other languages; but in all cases you are "adding" two things. This doesn't have to mean you are performing arithmetic based addition; but the general use cases for it remain the same; which is taking some value and adding an additional value. That may change the magnitude, the items in the set, or something else; but the algorithm you are doing with it remains generally the same. You have a T = T + T; and you could for example do foreach (T item in array) { result += item; }.

There are many other languages outside of .NET that allow you to define shapes/traits/roles/etc around individual methods or operators and they always extend far beyond just numerics.

You give two motivating examples. Both involve numbers. (As an aside, I think it would be instructive to write out the second example in terms of generic math instead of relying on LINQ's generic Sum.)

The second example uses generic math and it is a completely valid use case of it showing a likely real world scenario. There are other examples elsewhere, such as in the blog post: https://devblogs.microsoft.com/dotnet/preview-features-in-net-6-generic-math/

There is a balance between providing a couple simple examples to relay the idea and providing something that is "complete" and fully describes the idea, use cases, and scenarios.

If I spend all my time doing the latter, then I have less time to actually implement the feature and ensure it can ship.

When you define an interface, that must mean something. IComparable has one method, but that one method allows you to define an order for your type, sort collections... It's a very general concept with a very clear purpose that means the same thing everywhere.

Yes; and the same applies to all of the operator interfaces exposed :)

The difference is that IAdditionOperators means "I provide a + operator" while some want it to mean only "I support arithmetic based addition".

The same is not true for operators. Does it make sense to define a single interface that covers both generic math operations and operations "that don't fit the normal model for mathematics, numerics, or even recommended practices"?

Yes, because the operator interfaces are not restricted to numerics. They are general building blocks.

The numeric interfaces take those interfaces, build on top of them, and give them well-defined semantics related to numbers.

In addition, for generic operands (like Vector), I find there is much less of a need for generic interfaces on that level because you usually control the entire type hierarchy and you can define operations in terms of operations on elements. See, for example, Silk.NET, which has several variations of generic vector/matrix products. The types in System.Numerics.Vectors only define the standard operators (+-*/[=!]=).

There are many things that go into this, including back-compat; existing generic constraints, and the intended usage scenarios for the types.

@maxild
Copy link

maxild commented Jan 4, 2022

There may be a slightly different split, but it is not likely to grow more significantly. The BCL will provide the "core" interfaces allowing other users to build their own types and cover common algorithms and type interchange scenarios. Users interested in more specialized scenarios can then define their own interfaces built on top of the interfaces we provide or even their own hierarchies.

Users can't define their own hierarchies. That would be very painful to wrap all the primitive number types.

@tannergooding
Copy link
Member Author

I am not sure, but I think .NET 1.0 brought Decimal over from oleautomation (VB 6, VBA, COM).

Yes

And IEEE 754 (2008) created decimal types, based on the IEEE idioms of normal and subnormal numbers (denormalized numbers around zero and NaN, +inf and -inf). @tannergooding You know more about IEEE 1985, 2008 and 2019 than I do, so please correct me if am wrong.

No. IEEE 754 (2008) merely merged the IEEE 754 (1985) and IEEE 854 (1987) standards. The core principles around "base-10 IEEE floating-point" have been around for a long time.

Both IEEE decimal and .NET decimal are "Currency" type (base 10).

No. While base 10 numbers can generally be used for currency; the intent of System.Decimal is that it is explicitly a currency type (the C Windows SDK even has a typedef for it of CY, among others) and intended to be used almost exclusively in monetary like scenarios. The underlying format and representable values restrict its usage in other domains.

The IEEE decimal types, on the other hand, were explicitly designed for general purpose usage including in domains where the representation of values such as infinity, nan, and -0 are important.

Since it is unlikely that the BCL will support IEEE 754 decimals, I urge you to consider giving System.Decimal (that does not have the "IEEE idioms") a place in the INumber hierarchy (probably a common base interface of IFloatingPoint, because both are fractional numbers). This was what dotnet/runtime#62293 was all about (I don't think I stated that very clearly, I am afraid)

System.Decimal already has a place in the hierarchy and I have stated that I will look at moving its place in the hierarchy so concepts like rounding can still be exposed.

@maxild
Copy link

maxild commented Jan 4, 2022

There are many other languages outside of .NET that allow you to define shapes/traits/roles/etc around individual methods or operators and they always extend far beyond just numerics.

Like you said. .NET/C# is not one of those languages. If I built a "different" number hierachy, how can I extend the BCL types using "static abstract in interfaces"?

@tannergooding
Copy link
Member Author

Users can't define their own hierarchies. That would be very painful to wrap all the primitive number types.

Users can and will define their own hierarchies and interfaces. In some cases, this will be intentionally not related to numerics at all and will build on the operator interfaces.

In others, it will likely extend the baseline functionality that we provide for the numeric interfaces with additional concepts specific to their library or domain.

@maxild
Copy link

maxild commented Jan 4, 2022

We certainly could update it to meet that contract and to ensure that DivRem the integer division and remainder result or we could move DivRem to be integer specific, both are valid options here.

In our code (that is unfortunately closed source) we have "division with remainder" defined as a single function working for both integers and fractional numbers. I would personally prefer that.

@maxild
Copy link

maxild commented Jan 4, 2022

No. While base 10 numbers can generally be used for currency; the intent of System.Decimal is that it is explicitly a currency type (the C Windows SDK even has a typedef for it of CY, among others) and intended to be used almost exclusively in monetary like scenarios. The underlying format and representable values restrict its usage in other domains.

Interesting. I use decimal for a lot of other things, besides money. I am not talking "numerical methods", but as long as your "rational number" (fraction, significand) have a denominator with a power of 2, 5 or 10, you are good, right? @tannergooding You are probably right here, like I said I am not an IEEE expert. Does it have to do with the reasons that base 2 IEEE 754 numbers are (often) great:

  • insanely many numbers around zero (the denormalized numbers)
  • NaN, and error propagation
  • Symmetric values
  • +0 and -0
  • +Infinity and -Infinity

Are IEEE 754 decimals (plural, there are more sizes/precisions, compared to C# decimal) somehow better than System.Decimal, or maybe more versatile (I mean suited for more applications)?

@tannergooding
Copy link
Member Author

tannergooding commented Jan 4, 2022

Are IEEE 754 decimals (plural, there are more sizes/precisions, compared to C# decimal) somehow better than System.Decimal, or maybe more versatile (I mean suited for more applications)?

They support NaN and Infinity, both of which are effectively required for operations like Sqrt and Transcedentals (cos, sin, tan, etc).

They support and expose -0 (decimal supports, but does note expose -0), this is very important when dealing with rounding as it can help represent a negative value that rounded to zero preserving stateful information that is important due to the imprecise nature of the values.

They have more formats (decimal32, decimal64, and decimal128 at least) which provides more options based on required precision and speed as well as having better utilization of the underlying bits and therefore allowing larger ranges of values to be encoded.

  • System.Decimal is 128-bits and can represent values with up to 29 significant digits.
  • decimal32 is 32-bits and has up to 7 significant digits
  • decimal64 is 64-bits and has up to 16 significant digits
  • decimal128 is 128-bits and has up to 34 significant digits

The IEEE decimal types also have a format that is "more trivial" to perform certain operations on; making them typically faster than System.Decimal particularly for things like Sqrt, Transcedentals or values involving many significant digits.

System.Decimal however, has a format that makes some of these operations "difficult" to perform however and where the more significant digits you have, the slower the operation may be. In an ideal scenario, you aren't going beyond 3-4 fractional digits for System.Decimal and that falls inline with using it as a currency type.

@tannergooding tannergooding merged commit 7b10af9 into dotnet:main Apr 2, 2022

static abstract TResult checked operator %(TSelf left, TOther right);
static abstract TResult operator checked -(TSelf left, TOther right);
}

public interface IUnaryNegationOperators<TSelf, TResult>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this should be named IUnaryMinusOperators to match IUnaryPlusOperators. That's what the operator is called: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/arithmetic-operators#unary-plus-and-minus-operators

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These match the IL names, not the C# names. The IL name is op_UnaryNegation

Copy link

@Neme12 Neme12 May 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ok. Although I still think most languages would call it unary minus. Most people don't know IL and don't write it, so if this name exists only in IL, I'm not sure that's a good source to go by.


// Logical right shift
static abstract TResult operator >>>(TSelf value, TOther shiftAmount);
static abstract TResult AdditiveIdentity { get; }
Copy link

@Neme12 Neme12 May 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be TSelf as opposed to TResult? I think it would be really weird if a number type had a static AdditiveIdentity property that is of a different type. And conceptually, this doesn't feel like a result anyway, but rather like a value that you use as an input to the + operator, and in IAdditionOperators, one of the parameters is actually of type TSelf. Same with MultiplicativeIdentity.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider things like DateTime + TimeSpan where TSelf is DateTime but the additive identity is a TimeSpan

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, you're right. It's still not a result though, so the name TResult feels wrong. Maybe I would call this TOther - in DateTime, TimeSpan is the TOther in the operator.

@Neme12
Copy link

Neme12 commented May 4, 2022

@tannergooding I watched the API review (https://www.youtube.com/watch?v=Qo40CuHxSwk) and have a few thoughts and suggestions.

  1. I think IFloatingPoint should be renamed to something else that doesn't imply that the number is represented with a floating point. All the methods on it apply to any decimal (non-integer) number types. For example, they would apply to and be useful on numbers with a fixed-point representation or rational numbers as well. And if these types are supposed to implement it, the name IFloatingPoint is misleading. Something like IDecimalNumber might be more appropriate (although the name might suggest that it's stored in base 10). Or IFractionalNumber. Maybe someone can come up with a better name, but I think IFloatingPoint is very misleading.

  2. It seems there should also be an IInteger interface. This method, currently on IBinaryInteger:

    static abstract (TSelf Quotient, TSelf Remainder) DivRem(TSelf left, TSelf right);

    could move up to IInteger because I don't see why it's specific to integers that are represented in binary. This method has nothing to do with the representation and is useful for any integers. Also, IInteger could then also have IsEven and IsOdd methods, which don't make sense and don't have a defined behavior for numbers other than integers, so shouldn't be on INumber or INumberBase.

  3. I think these properties:

    static abstract TSelf E { get; }
    
    static abstract TSelf Pi { get; }
    
    static abstract TSelf Tau { get; }

    should be moved from IFloatingPointIeee754 up to IFloatingPoint. They would be useful in any number types that can represent non-integers and nothing about them is specific to floating-point arithmetic.

  4. I think these methods:

    static abstract TSelf Abs(TSelf value);
        
    static abstract bool IsNegative(TSelf value);
    
    static abstract TSelf MaxMagnitude(TSelf x, TSelf y);
    
    static abstract TSelf MinMagnitude(TSelf x, TSelf y);

    should be moved from INumber up to INumberBase. Absolute value, or the magnitude (and therefore also MinMagnitude and MaxMagnitude), isn't specific to scalar numbers. It's defined for multi-dimensional number types like complex numbers or quaternions as well. Same applies to the concept of whether a number is negative.

  5. What about INumberBase types that support parsing with NumberStyles? I think INumberBase should ideally derive from ISpanParsable and contain the NumberStyles overloads for parsing, but if it doesn't, it might be useful to have another interface that contains the Parse/TryParse overloads that take NumberStyles. That way, if someone has a complex number type (and therefore cannot implement INumber) that also supports parsing, they could implement ISpanParsableNumber/INumberStylesSpanParsable in addition to INumberBase, so that these methods could be used in a generic context.

  6. I really think that IEqualityOperators should be in System rather than System.Numerics, not only to match IEquatable, but because equality (including equality operators) has nothing to do with numbers or numerics. It could be implemented by System.String, or really any type that is equatable, like most structs. In fact, I can even imagine a future analyzer which would suggest implementing the interface if you have the operators. Right now, we might think that it's not useful to implement it for other types, but there might someday arise a scenario where it's useful, and even if we don't have one, I'd say that it just has nothing to do with numbers, so it doesn't make sense in System.Numerics to me. I think the same is true for IComparisonOperators - a lot of things other than numbers are comparable. All the other operators are based on mathematical operations, so it makes sense that all of the other ones are in System.Numerics.

  7. In the API review (at 2:15:50), it was suggested that the Create method would take a converter and there should also be an overload that doesn't take a converter and would call the other method with NumberConverter.Default:

    struct Int32
    {
        public static int Create<TOther>(TOther value)
            where TOther : INumber<TOther>
        {
            return Create(value, NumberConverter.Default);
        }
    
        public static int Create<TOther, TConverter>(TOther value, TConverter converter)
            where TOther : INumber<TOther>
            where TConverter : INumberConverter<TOther, int>
        {
            return converter.Convert(value);
        }
    }

    This code wouldn't compile. The Create overload without a converter cannot call the other method with NumberConverter.Default because NumberConverter.Default cannot be used as a converter that takes an arbitrary TOther - it might not implement INumberConverter<TOther, int> from the TOther that this method is called with. There will be a compiler error on the invocation.

  8. In the API review (at 2:08:27), the sample implementation of Int32.Create (without the INumberConverter approach) throws if TFrom isn't a built-in numeric type or IBinaryInteger, and @tannergooding basically says that it has to throw in the worst case because it cannot support arbitrary other number types that it doesn't know how they're represented. This is also presented as the motivation for the INumberConverter approach. That's not quite true though. For example, for UInt32, you could create a Create implementation like this, which would be able to create a uint from any number type regardless of its representation by calculating the binary digits one by one:

    struct UInt32
    {
        public static uint Create<Other>(TOther value)
            where TOther : INumber<TOther>
        {
            uint result = 0;
    
            for (int i = 0; value > 0; ++i)
            {
                if (value is IFloatingPoint<TOther> floatingPointValue)
                    value = floatingPointValue.Truncate(); // I realize this won't work because it's a static method, but didn't want to think about this too much.
    
                result |= (value % 2) > 0 ? 1u << i : 0u;
                value /= 2;
            }
    
            return value;
        }
    }
  9. Now that I'm seeing this, having to do something along the lines of if (value is IFloatingPoint<TOther> floatingPointValue) in the code sample above makes me think it might be useful for some of the methods on IFloatingPoint (or maybe all of them because they're all related to rounding) to be moved to INumber. They would be a noop on integers and only be implemented explicitly there, but it's still a well defined behavior and in generic algorithms where you don't know whether you're dealing with a number that is an integer or not, you might want to round the value just in case it's not an integer.

I hope at least some of these ideas might be useful. 😅

@tannergooding
Copy link
Member Author

I hope some at least of these ideas might be useful. 😅

Thanks for the feedback!

  1. I think IFloatingPoint should be renamed to something else that doesn't imply that the number is represented with a floating point. All the methods on it apply to any decimal (non-integer) number types. For example, they would apply to and be useful on numbers with a fixed-point representation or rational numbers as well. And if these types are supposed to implement it, the name IFloatingPoint is misleading. Something like IDecimalNumber might be more appropriate (although the name might suggest that it's stored in base 10). Or IFractionalNumber. Maybe someone can come up with a better name, but I think IFloatingPoint is very misleading.

I agree this isn't the best name and doesn't necessarily cover types like "Rational" or "FixedPoint". There, unfortunately, isn't a lot of names to choose from and there probably isn't a good name that encompasses everything people want.

IFloatingPoint at least covers the mainstream scenarios and leaves room to expose IFixedPoint or others in the future if that's desirable.

If someone does have name suggestions, I'm open to hearing them. Noting that IDecimal is a non-starter due to a specific meaning around Decimal and IEEE 754 decimal types. IFractionalNumber likewise has its own concercns.

  1. It seems there should also be an IInteger interface. This method, currently on IBinaryInteger:

I will raise this one in API review but there is currently no plans to expose or support any concept of a non-binary integer. Unlike with floating-point types, there is little benefit to having something like a decimal-based integer.

  1. I think these properties:

These are explicitly left-off today because there are no plans to ever expose these on System.Decimal. It, as a type, is meant to support currency and not more advanced numerical computations as E, Pi, or Tau may imply.

It may be worth pulling these into their own IFloatingPointConstants interface or similar, but that would require more thought.

  1. I think these methods:

Abs is likely going to be moved down already. IsNegative may be as well. Max, MaxMagnitude, and others do not make sense on types like Complex as they are not normally considered "ordered" and therefore not comparable.

  1. What about INumberBase types that support parsing with NumberStyles?

The plan is to move ISpanParsable down. One of the potential issues is that parsing for things like Complex can get quickly complicated.

  1. I really think that IEqualityOperators should be in System rather than System.Numerics, not only to match IEquatable, but because equality (including equality operators) has nothing to do with numbers or numerics.

For most of these, IEquatable should be the interface of choice. Not only is it already there and broadly available, but it will more often than not do the correct thing for .NET. Where-as == is generally more numerically inclined and is not valid for use in things like collections or hash sets.

  1. n the API review (at 2:15:50), it was suggested that the Create method would take a converter and there should also be an overload that doesn't take a converter and would call the other method with NumberConverter.Default:

The code represented was pseudo-code. The actual API surface should get reviewed next Tuesday.

  1. In the API review (at 2:08:27), the sample implementation

This is invalid for many reasons, primarily because doing a if (value is ISomeInterface) check does not give you access to the static methods on ISomeInterface.

Additionally, it presumes that the implementation and behavior here is "well-defined" for %, <<, and friends when it may in fact not be.

The general purpose fallback path is going to involve using the TryWriteLittleEndian, TryWriteExponentLittleEndian, and TryWriteSignificandLittleEndian APIs to get the raw two's complement bits, allowing a general fast path for many scenarios.

This still leaves a gap where two libraries that aren't familiar with eachother can't have their types convert between eachother and is why we fundamentally require the ability to define some external converter (INumberConverter)

9 Now that I'm seeing this, having to do so

#262 covers moving some of these APIs around where they make sense and it is valid for it to be a no-op or return a constant (e.g. always return false)

@Neme12
Copy link

Neme12 commented May 4, 2022

Abs is likely going to be moved down already. IsNegative may be as well. Max, MaxMagnitude, and others do not make sense on types like Complex as they are not normally considered "ordered" and therefore not comparable.

Yes, Max requires comparisons on the type, but as far as I understand it, MaxMagnitude compares the absolute value of the number, not the number itself. And the absolute value is always a scalar.MaxMagnitude could definitely be implemented for types like complex numbers or quaternions.

@SpocWeb
Copy link

SpocWeb commented May 5, 2022

@Neme12 I also requested an Abs or Norm Method on INumberBase, similar to your proposal 4, because, most Math Objects have a (usually positive definite) Norm and this Norm is needed to check for convergence in very many algorithms. Basically the Norm defines a Topology which is a very general and useful concept.

@SpocWeb
Copy link

SpocWeb commented May 5, 2022

@tannergooding I suppose MaxMagnitude and MinMagnitude are new Names for the same semantic as the existing MaxValue and MinValue Methods on most Number Types?
I wonder though why you don't reuse the well-established Names.

As @Neme12 's Question shows there is a Need for comparing the (often squared) Magnitude of mathematical entities to determine whether the difference between two is 'small'.
It would be great if all primitive Number Types would implement such a Function via a common Interface!

The Magnitude would be an IComparable Number. I actually have good experience just using double instead of TResult, that makes it easier to define approximate Equality and Convergence independent of the concrete INumber Type.
So my proposed Signature of a IHaveNorm Interface would be double Magnitude(TSource) or double Abs(TSource) with a Semantic that all existing (scalar) Number Types return their absolute Value, which should fit into a double, except e.g. BigInt whould should yield double.PositiveInfinity when exceeding the Range.
Loss of Precision is also possible with BigInt, but that is acceptable, because the Magnitude would not be used for Norming the Number, but for Checking Proximity and for that mostly the Scale/Exponent matters, not the Mantissa.

@SpocWeb
Copy link

SpocWeb commented May 5, 2022

@Neme12 Any Details why the Thumbs down?

@tannergooding
Copy link
Member Author

I suppose MaxMagnitude and MinMagnitude are new Names for the same semantic as the existing MaxValue and MinValue Methods on most Number Types?

No. MaxValue and MinValue are properties (or constants) that return the maximum and minimum representable values, repesctively.

Max, MaxNumber, MaxMagnitude, and MaxMagnitudeNumber (as well as the corresponding Min variants) are functions. Where:

  • Max returns the maximum of x and y. For types with NaN values, the NaN is propagated (returned) if either input is NaN
  • MaxNumber returns the maximum of x and y. For types with NaN values, the NaN is not propagated, the numeric value is returned instead (unless both are NaN, in which case NaN is returned)
  • MaxMagnitude and MaxMagnitudeNumber do the same operation but compare the absolute values of the inputs (still returning the original input, so MaxMagnitude(2, -3) returns -3).

These are well-established names set by the IEEE 754 standard and which have been adopted by other languages.


The Magnitude would be an IComparable Number.

This is not feasible due to not having something like "associated types". Abs currently needs to return TSelf and so Complex.Abs is going to return a Complex where the imaginary part is always 0 (and therefore defines a real-number).

MaxMagnitude and MaxMagnitudeNumber could be exposed on INumberBase with this limitation. It would just restrict them from having some default implementation provided.

Alternatively someone could define some IComplex<TSelf, TScalar> interface that builds on top of INumberBase and extends it further.


These interfaces cannot cover "every" type people may want to support and no language that provides similar interfaces does either.

The goal is to cover a majority of needs for users around common types and programming patterns. It is explicitly not a design goal to fit any mathematical model, as computer math rarely actually matches "real math". It's instead only a rough approximation with various limitations and additional rules.

@Neme12
Copy link

Neme12 commented May 5, 2022

MaxMagnitude and MaxMagnitudeNumber could be exposed on INumberBase with this limitation. It would just restrict them from having some default implementation provided.

If we want a default implementation, it could be added to INumber - the interface that most users would presumably implement. Those only implementing INumberBase would need to provide a specialized implementation, but it still shouldn't be an issue for them.

@Neme12
Copy link

Neme12 commented May 5, 2022

@Neme12 Any Details why the Thumbs down?

Because of the points that @tannergooding raised. Although I still think the absolute value could be made useful even on TSelf where TSelf is not an INumber. if INumberBase had the following APIs:

public static bool IsReal(TSelf value); // INumber defaults this to true
public static IComparer<TSelf> RealValueComparer { get; } // Alternative name: RealPartComparer. Compares the real part only. INumber defaults this as well.

and/or this:

public static IComparer<TSelf> AbsoluteValueComparer { get; }

but this would require introducing the notion of a comparer - INumber currently does not derive from IComparable<T>, which means that 1) INumber couldn't implement that comparer (implementing it using operators would be wrong), and 2) it would be weird that only this specific part of the API uses a comparer interface.

@SpocWeb
Copy link

SpocWeb commented May 5, 2022

@Neme12 I would like to see this implemented on the existing primitive Types, so I don't have to write Functions to treat them special. I could e.g. write while ((x-y).Norm() > 1e-9) { ...continue refining } for both primitive Types like double, float, Complex and for custom Types.

@Neme12
Copy link

Neme12 commented May 5, 2022

@SpocWeb Why do you want Norm as well? Wouldn't Abs be the same thing?

@tannergooding
Copy link
Member Author

tannergooding commented May 5, 2022

I could e.g. write while ((x-y).Norm() > 1e-9) { ...continue refining } for both primitive Types like double, float, Complex and for custom Types.

You're thinking about this too much from the abstract mathematical sense. 1e-9 might not be representable by T and that's something you fundamentally need to handle and account for.

Various types may have compounding error over many operations or other issues (such as having different epsilons between values based on the input with the largest magnitude).

While you can write many types of generic algorithms using these interfaces, it doesn't remove the need to consider types, kinds, or limits. These all still need some minimal amount of consideration.

@SpocWeb
Copy link

SpocWeb commented May 9, 2022

Wouldn't Abs be the same thing?

Not quite as @tannergooding explained above, TSelf Abs(TSelf) returns the same Type as its argument, which cannot be used in Comparisons.

Instead, 'Norm' should always return an INumber which is IComparable<T>.
Unfortunately there is no canonical 'associated' INumber Type for each INumberBase, which makes handling these INumber Results quite difficult, because you may need to compare Instances of different INumber Types.

Therefore I think a primitive Number Type like double would make comparisons easier and more efficient, similar to int CompareTo<T>(T other) which also uses a primitive Type. The huge Range of double should be sufficient for all practical means. The Precision is not so important.

@Neme12
Copy link

Neme12 commented May 9, 2022

@SpocWeb I don't it should be a fixed type like double. It might not be good enough for everyone. But you should be able to compare the absolute value if the functions MinMagnitude and MaxMagnitude exist on INumberBase. Or you could just convert it to a double if you really want to.

For CompareTo, you only really need 3 values (-1, 0 and 1), that's why int is sufficient. You don't need to return arbitrary numbers.

@SpocWeb
Copy link

SpocWeb commented May 9, 2022

@Neme12 Doing such a conversion to double in a generic Algorithm<TSelf> is not possible.
Most engineering and mathematical objects have a Norm. Think of Complex, Vectors, Matrices, Tensors etc.
These are inherently not IComparable because they can only be INumberBase.
Since MinMagnitude returns the same Type, you can still not compare them.
I would be satisfied with any concrete INumber Type as long as I can use it in any Algorithm<TSelf> independent of the concrete TSelf.

@Neme12
Copy link

Neme12 commented May 9, 2022

Since MinMagnitude returns the same Type, you can still not compare them.

MinMagnitude returns the number that has a lower absolute value/norm, so yes, it does comparisons.

@SpocWeb
Copy link

SpocWeb commented May 9, 2022

@Neme12 Ah, that's an Idea, so you propose to to something like this:

TSelf goal = initial * desiredAccuracy;
TSelf next = initial;
while (goal != goal.MinMagnitude(next)){
    next = Improve(next)
}

Yes, I presume that would work, although the Equality-Test looks awkward (and may be prone to subtle changes if not returning an exact copy of one of its arguments) compared to the same with a proper Norm:

TSelf goal = initial.Norm() * desiredAccuracy;
TSelf next = initial;
while (goal < next.Norm()){
    next = Improve(next)
}

Thanks for the idea!

@SpocWeb
Copy link

SpocWeb commented May 9, 2022

So I would be happy to have at least MinMagnitude on INumberBase although in the example above it incurs an additional Equality Comparison (first you compare goal and next using MinMagnitude and then you compare that Result again with goal) which incurs inefficiencies in such a tight loop.

@SpocWeb
Copy link

SpocWeb commented May 10, 2022

Although of course you can easily define both MinMagnitude and MaxMagnitude using the single Norm Function and I would rather only implement the Norm for new Types and the other two as Extension Methods.

TSelf MinMagnitude<TSelf>(TSelf left, TSelf right) => left.Norm() < right.Norm() ? left : right;

TSelf MaxMagnitude<TSelf>(TSelf left, TSelf right) => left.Norm() > right.Norm() ? left : right;

If it wasn't for the Fact that Complex already defines a Magnitude Property with the Signature I imagine, we could use that Name instead of Norm. To me, a Rose is a Rose...

@tannergooding
Copy link
Member Author

It isn't sufficient to define the implementation like that because it doesn't correctly take into account things like -0 or NaN

@SpocWeb
Copy link

SpocWeb commented May 10, 2022

Yes, but that can surely be extended I imagine, depending on how MinMagnitude and MaxMagnitude are supposed to handle these cases.

@JeppeKjeldsen
Copy link

Hopping into this late because I recently had to work with generic numerics.

What I would have liked would be an IOperators<+, -, *, /, %, ==, !=, >, <, <=, =<> interface which would describe which operators was implemented and enforce their implementation, this way you could ensure that a generic type always had a specific operator.

I don't know if something like this would be possible with how interfaces currently works, but if it's possible I imagine it would be a much more humanly readable way to implement this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.