-
Notifications
You must be signed in to change notification settings - Fork 162
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
Adding a rough draft of the "minimum viable product" for the .NET Libraries APIs to support generic math #205
Adding a rough draft of the "minimum viable product" for the .NET Libraries APIs to support generic math #205
Conversation
This does not currently capture notes around # of interfaces impacting startup, around requirements for DIM support to enable versioning, any real overview of how concepts like |
* System.Tuple | ||
* System.ValueTuple |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ValueTuple is instantiated a lot. Adding more interfaces to it is very problematic performance-wise.
public interface INegatable<TSelf, TResult> | ||
where TSelf : INegatable<TSelf, TResult> | ||
{ | ||
// Should unary plus be on its own type? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If not, this seems like a good case for supporting default implementations since (I think it's safe to say) 99% of implementations of unary +
will just return value
unmodified.
where T : struct, IAddable<T> | ||
{ | ||
ThrowHelper.ThrowForUnsupportedVectorBaseType<T>(); | ||
return left + right; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In your opening sentence you wrote "an important numeric helper type for SIMD acceleration". How does this implementation play in with SIMD?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SIMD is basically just doing a given scalar operation n
times, where n
is the number of elements in a hardware vector.
To provide a software fallback we have to support scalar operations over arbitrary T
and so today that involves doing if (typeof(T) == typeof(...))
checks everywhere.
Generic math is one way that can be simplified, as many helpers could be simplified down to just where T : INumber<T>
// This, in a way, obsoletes Math/MathF and brings float/double inline with how non-primitive types support similar functionality | ||
// API review will need to determine if we'll want to continue adding APIs to Math/MathF in the future | ||
|
||
static abstract TSelf Acos(TSelf x); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are many of these static rather than instance? i.e. I'm not sure why this isn't just TSelf Acos();
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because these kinds of operations have traditionally been static
in .NET.
Also, there is a perf consideration here because this
is byref
(or inref
for readonly struct
) which can hurt inlining and other optimizations done by the JIT.
{ | ||
public struct Byte | ||
: IBinaryInteger<byte>, | ||
IConvertible, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth denoting somehow that some of these interfaces are already implemented by these types.
|
||
There are several types which may benefit from some interface support. These include, but aren't limited to: | ||
* System.Array | ||
* System.DateOnly |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You already included this one above. Same with TimeOnly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Forgot to remove them from this list when I added them above. Will fix.
* System.TimeOnly | ||
* System.Tuple | ||
* System.ValueTuple | ||
* System.Numerics.BigInteger |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd have expected this one to be a given.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Several of them are "given", but I haven't done any scenarios for them yet and so am just tracking them below until I can finish writing the code locally.
* System.Numerics.BigInteger | ||
* System.Numerics.Complex | ||
* System.Numerics.Matrix3x2 | ||
* System.Numerics.Matrix4x4 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are lots of comments throughout the interfaces about how various decisions are being made in order to support types like matrices, so I'd hope we either implement the interfaces on matrices or don't constrain ourselves by things we aren't ourselves willing to do :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This list is more a todo
than a "maybe we should think about these".
Even if we decided to not support our own types, for whatever reason, ensuring the interfaces work for 3rd party types is an important scenario.
(I do plan on defining interfaces and including them in the design doc here, but since I only got compiler bits a couple days ago, it was relatively slow going to "validate" the designs 😄)
|
Putting a note here on Maybe it is better to leave sets out of scope for these interfaces. |
Sorry for asking so late in the design time, but what is an advantage in not treating a From the user's point of view, a From the user's point of view, having a pair of values inside is in most cases just an implementation detail, the same as having several backing fields in By the way, |
(I'd argue that increment doesn't make much sense for non-integers. For |
Complex - "scalar" needs a definition
Nothing in the spec, name, blog articles on INumber indicate this is a hardware supporting-feature restricted to SIMD or similar.
A Complex number is a single concept both in mathematics and in programming (single type), and your assumption that having two fields prevents this is unjustified.
The only concept around scalar in the design doc is the use of Take inspiration from Swift
The doc specifically mentions Swift, but Swift has a much more logical approach here, which in particular pays considerable respect to mathematics, and which in particular doesn't have the first three decisions that you reference (requirement for parsing/formatting operations, type-unsafe conversion, complex numbers aren't scalars/numbers):
(I am not a Swift programmer so please correct me if I am wrong here.) From the Swift specs, its numeric types have been much better designed copying over the Swift spec would be a drastic improvement. It has a good combination of mathematical conformance, type safety, and simplicity. The conformance of the interface hierarchy to mathematical concepts is icing on the cake. The dotnet process is currently heading for something much worse because criticism from the community is not engaged with seriously. The differences are going to be:
What in the dotnet type system prevents implementing INumber correctly? In particular there is no requirement for parsers on any type, no requirement for conversion between two different types, and no requirement that a type has only one field. |
Methods can require multiple constraints, which eliminates need in one complex interface: void PrintSum<T>(IEnumerable<T> items) where T : INumber<T>, IFormattable
{
T sum = items.Sum();
Console.WriteLine(sum.ToString(CultureInfo.InvariantCulture));
} If all operations are actually needed under one interface, then create rich interface: Keep simple things simple |
+1 for the Swift model. Seems that this discussion is not appealling to a great numbers of dev, but if it were the case, I believe that most of them would like the cleanest possible model that handles beasts like Complex or other multidimensional "numbers" rather than a simplistic one. This is a low-level aspect that aims to be used by senior/framework/kernel/core developpers, not on an applicative layer that must be "easy for juniors to jump in". |
I've tried to respond to your points. This became a bit of a long response so I've put it in a collapsible region here and followed up with a second post covering more direct examples that address everyone more generally.
I'd agree that this is the case from a higher level overview of a "complex' number, but not of the
This is actually a good reason on why it is separate. Also consider that these interfaces need to be semi-extensible for the future. We do not want to be introducing Let's drill down into some specific examples of APIs like: For scalar numbers, if (x < 0) return -1;
if (x == 0) return 0;
return +1; Likewise, for return (Sign(x) == -1) ? -x : x; For Likewise, operations like So once you drill down into things, if you want And if you're thinking removing those other members is too much or taking the idea too far, there has been feedback asking for exactly that as well. These interfaces will not fit the wants of everyone, they will end up defining a common contract that is believed to be beneficial to the majority of devs, within the limitations we have for designing and exposing such a system.
The standard practice is that
|
I've tried to respond to your points. This became a bit of a long response so I've put it in a collapsible region here and followed up with a second post covering more direct examples that address everyone more generally.
You're taking the term computer hardware too literally here. I meant in the general sense of computer science/programming and not mathematics. Programming borrows many concepts from mathematics and then "simplifies" or otherwise twists it around a bit to fit the needs and appropriate use cases. Computer Math != Real Math, even though various terms and concepts are similar. You have a finite approximated system where things like rounding or overflow have to be considered. Because of this, concepts like rings, groups or fields do not translate well into most programming languages.
Yes, a single type. But one that is logically two parts. This is also not two parts in the sense of two fields.
For
Swift does indeed expose several things differently here. Part of this because it was designed with things like associated types as first class and implemented this a lot earlier.
The
Indeed. However, the design notes also calls out various considerations that had to be made. You cannot do It basically throws out the concepts of It discusses some of the tradeoffs in implementing the Due to various reasons, including back-compat,
I am very much engaging in this seriously and considering the input that's been given. At the same time, I'm trying to explain as the area owner why I believe that input isn't as impactful as the relatively few who are stating it believe it to be. Swift has a good design here, but it also hit limitations and other issues that .NET can avoid by dealing with up front. Likewise, there are certain aspects that won't translate over well, even where they would be nice, because .NET doesn't have a corresponding concept (such as associated types). Adding such features has been considered but in the most likely scenario, wouldn't make it for this feature or worse would delay this feature for a couple more years at least.
Making these interfaces useful for the majority of developers in .NET is indeed a use case and consideration here. It's nice to be able to define libraries that follow mathematical concepts and have generic algorithms. It simplifies your code, makes it easy to use, etc. However, once those libraries get into the hands of downstream users; they need to actually be able to use the types. It would be nice if the functionality they needed was likewise available by default. To that end, most types need or are expected to support things like Such functionality allows you to do "basic" operations with types in .NET, including storing it in a collection or dictionary and displaying some readable value to the user (Debugger, Console, GUI, etc). Some types extend this functionality a bit further by supporting additional formatting modes ( These interfaces will allow users to extend and expose additional contracts on top that expose other interesting functionality.
There are real world costs to the number of interfaces exposed on a type and it can have measurable impact on startup and usage of the type, such as when doing type checks or casts. Particularly when these interfaces are on the primitive types, which every app will use, we have to be careful about how many we're exposing. Likewise, .NET doesn't support concepts like associated types (and likely won't be getting them for this feature) and so certain concepts from languages like Swift don't translate over well. While there are indeed no requirements that a type implement these features, they are semi-basic functionality when dealing with numbers or types in most real world contexts, which I'll get to in a follow up post here. |
To elaborate a bit more on the above and give specific examples, let's consider that there are roughly two groups that these interfaces have to appeal to. First, these interfaces need to appeal to library and framework authors who are defining their own types and want other libraries or applications to be able to use them. This group likely wants a number of fine grained interfaces that it can fit exactly to its needs and not have to extend the scope of their support to additional concepts. Second, these interfaces need to appeal to application and service authors who are trying to provide a functioning app to their userbase. This group is somewhat in the opposite camp and likely wants a number of large grained interfaces so they get the most functionality without having to specify My job is to ensure that both groups get their needs met, that the interfaces are extensible for the future, and that various limitations of the .NET type system are considered. This includes considerations that each interface exposed, particularly on the primitive types like Now, lets go over some of the most simplistic apps that every beginner needs to learn and therefore what might be expected for beginners to be able to use these types. Most beginners start out on the To this extent, one of the first things you learn to do in programming is:
Thus, one of the simplest and most basic operations is serialization and deserialization of data. This extends to almost every real usage of a type in any application. It is prevalently shown in the usage of things like JSON, XML, YAML, in a majority of beginner tutorials for languages and application frameworks, and in how users interact with a computer, communicate with eachother, and input information via text on a keyboard and recieving text as a display on the other end. It is my belief that this is a core pillar to what is exposed on these interfaces and will be effectively a baseline expectation for many application authors to effectively use these types. That it will likewise play a core part in ensuring that beginners can easily learn about such things in a natural way and integrate it into their toolset. That without providing I don't believe there is any case where formatting a number type is inappropriate, at the very least it applies to providing a good debugging experience. I expect there are probably some cases where parsing isn't required or appropriate or unnecessary, but I also expect those to be a minority in the scheme of general applications and usages. It is therefore worth exposing as part of the core contract. Expanding our horizons a bit, lets consider the In the strictest sense, this algorithm doesn't need to do anything with external user input and so parsing is off the table. Formatting still extists for the purposes of the debugger. Likewise, this algorithm, mathematically speaking is relatively straightforward, just add things together until you nothing is left. Now, lets consider some scenarios that real world programs have to consider and where the basic math part breaks down. Starting with, unlike "real math", your When implementing such an algorithm you then have effectively three choices:
The first case is the default and while valid, most non-developers find it confusing when This brings us into the second "basic" operation that you need to deal with in computer programming: converting your data from one type to another (which is indeed very similar to the parsing/formatting case, perhaps there is a pattern here). It is my belief that this is also a core pillar to what is exposed on the interfaces. Number types are simply not useful on their own because we are in a finite system with bounds and limits and therefore in order to account for this while still allowing things to be efficient, we have many types each with their own different bounds and limits. In order to succesfully work with data, there are cases where you need to convert between types so you can have different more appropriate limits for your use-case. This is why Swift exposes the .NET tries to account for this by allowing conversions from Some of the wonkiness can also be mitigated by ensuring that We then get to cases like
Now representing Allowing It also brings in complexity around versioning. Things like To me, the changes being asked for above significantly reduces the usefulness of I do not believe this trade-off is worthwhile. Now, with that being said, there may be some other name we could use for |
Could you clarify a bit more what you mean by this distinction? If this is not a SIMD/hardware support concern, what would prevent a complex number from being represented as a "single logical set of contiguous bits" and why is the bit-by-bit representation important from the perspective of a numeric abstraction? Rationals are also represented using two distinct "parts", and the same can be said about floating point numbers. |
Let me try to put it into math terms. Integers, rational numbers, and floating-point numbers all represent This distinctly makes them different from other numbers that programmers or even the general population needs to consider and/or work with. They are an important, but rarely used subsection of the ecosystem that is relegated to specialized scenarios and I don't believe that trying to model the "primary" number systems in these interfaces is good or correct as it will likely lead to confusion and usability issues for the average developer who may not be familiar with terms like |
Where-as
It would also take the most obvious name |
I think the more important distinction for Complex is that it is an unordered field, rather than the fact that it has two components. Other numeric types that would require multiple components, such as rationals or quadratic fields are still ordered and can implement INumber quite naturally. I implemented my own Complex struct and had it implement INumber as currently defined, throwing NotImplementedExceptions for the operations that don't make sense. This worked and performed virtually identically with System.Numerics.Complex, but it's a kludge. I'm interested in some linear algebra applications that would benefit from creating generic matrices of either real (represented by double) or complex entries. As it is, to get this effect I have to specify some 9 or so interfaces, including IAdditionOperators, IMultiplyOperators, ISubtractionOperators, etc. Personally, I would like to see INumber restricted to the interfaces that make sense for Complex (i.e. the operations on an unordered field), and then have a derived interface like IOrderedNumber for all the types such as int, double, etc., which covers ordered fields. I won't make an argument about including the formatting and parsing interfaces. Those do make sense whether the number is from an ordered or unordered field, so it's more a matter of taste whether to include them or not. One final comment, the documentation for System.Numerics.Complex frequently and repeatedly calls it a number. It does seem like a violation of the Principle of Least Surprise, to then say well, it's a number, but not an INumber. |
I can definitely agree on this and we can update various documentation accordingly. I'd also like to reiterate that I am amenable to exposing some common interface to fit the needs here. However, I strongly believe that changing the current interface away from I'd suggest the interested parties propose, in tandem, two interface names here:
The second interface needs to be the obvious default choice for the general/non-domain specific users. The name of the first interface I have less concerns with, we could even call it |
In math terms, integers is a subset of floating point numbers, which is a subset of rational numbers, which is a subset of real numbers, which is a subset of complex numbers. Honestly the
That I can agree with. Ultimately it depends on whether we want to support things like comparison and magnitudes OOTB in the
Ultimately that depends on what customers we are trying to reach. This is likely true for the current .NET ecosystem, but I would dispute that it's rarely used if we're looking at catering to scientific computing applications. |
Personally, I would be happy with a "base" number interface that included essentially the operations for an unordered field, and which Complex and all the usual numeric types complied with. |
This is explicitly a feature targeting .NET as a whole. If it were being designed for domain specific usage, it would have a different design and would not be in the
Yes, if you go and look at a domain-specific industry then domain-specific terms and types become commonplace.
These are going to be supported. These are basic operations that I believe the majority of the .NET users will expect exists and will think of as being available when you tell them "you can work with number types via generics".
Yes, but we aren't targeting math users and that's one of the reasons I've tried to avoid putting these into math terms. We are targeting .NET programmers where the vast majority of developers are writing things like ASP.NET Web Sites/Services, WinForms/WPF/Line-of-Business apps, or making games in Unity. Math is an extremely specialized field and you can endlessly go down the rabbit hole of what is or isn't. If you asked the average developer, let alone the average person, if At the end of the day, and for many reasons, |
These are all fairly self-evident observations, but I'm not sure how they relate to the discussion at hand. At the end of day I'm making sure that any design trade-offs are made for the right reasons (and you did give quite a few valid ones here), but I will insist that the argument about |
I disagree and I don't know how to put into words that will give you the necessary insight into why I believe that's the case. Complex numbers are fundamentally different from the numbers or number representations that developers typically work with and they require additional consideration and special treatment when working with them.
interface IComplexNumber<TSelf, TElement> : INumberBase
where TSelf : IComplex<TSelf, TElement>
where TElement : INumber<TElement>
{ } It is its own unique thing in the same way that Some Part of this comes down to representation and part of it comes down to programs fundamentally needing to be able to create instances of a type and convert between types. |
Does this concern converting, say, |
Can we merge part of this? We just shipped and leaving such as huge design document open in PR for so long doesn't really help with making progress/catching up with changes. |
Merged. I'll get a new PR up with the .NET 7 changes here at some point in the near future. Further discussion can happen on that thread. |
I've opened #257 which I'll be updating to cover discussed changes, etc. As of right now, it only covers splitting out |
Coalescing the two prior API review notes for convenience
Notes from today's review:
|
I found that INumber Parse uses String as argument Can we go out of String in regard to ReadOnlySpan< Char > and/or ReadOnlySpan< Byte or Utf8String > to not allocate Strings where possible just for number parsing |
|
IMPORTANT: This is very much a draft of the API surface and there are several open questions and design points still to be resolved.
The current UML diagram looks something like:
There are four related language proposals for this feature:
There are also runtime related changes for this feature: