This is what I'm talking about. Create an empty repo on the official Swift account, start taking proposals. We could start with a Complex type and some basic statistics, with everybody acknowledging that the library is very early and anything might change (via an evolution process).
We haven't seen the TensorFlow stuff yet, but they have some cool-looking enhancements which the announcement said they're keen to up-stream. I'm guessing there are quite a few specialised mathematical structures and algorithms, and it would be nice to have a place for that to land where it would benefit the wider (non-TensorFlow) Swift community and help establish common-currencies.
Maybe the TensorFlow stuff will be the trigger to create a maths library.
I'd leave proposals out:
Allow everyone to create PRs, and merge automatically whatever reaches a certain amount of upvotes.
If this works, it could be possible to simplify Evolution a lot ;-)
It would be nice to base it first on an existing numeric API (like numPy for example) and set that API as a goal. Then the community has somewhat of a concrete objective s.t. everyone can push in the same direction.
I'm 90% in agreement here. I fully agree that we need swift-corelibs-math and a roadmap where it should go.
However, I think a complex number system and multidimensional arrays need to be outside of that, and resolved first, before work on a math library proceeds. The primary reasoning being that I think there do need to be a few small changes to the standard library to accommodate these data structures naturally and, hopefully, enable some cool compiler magic that can make these really fast.
I'm not nearly as well versed as everyone here on the details of Swifts design, but poking around, it looked hard to make the changes I was suggesting without some modification of the standard library.
I'm suggesting the FloatingPoint protocol have some intermediate protocol that doesn't include multiplication, division, and squareRoot(),
An Imaginary type would conform to this new protocol---but I'm not sure the best way to create that. It looks like the floating point structures are created from gyb, so it's not easy to just stamp out another floating point type without being in the standard library?
I'm sure there are other, better ways of doing what I'm suggesting.
Do you think it is reasonable/possible to include an Imaginary type in what you've done, that works with the existing system? By which I mean, naturally works with the existing floating type types?
The primary challenge I was seeing is that you'd couldn't let the Imaginary type conform to FloatingPoint, so you'd end up recreating a bunch of stuff.
I don't think the standard library protocols were designed to match varying mathematical concepts of numbers. There is "Numeric", which defines some basic operators with the expectation that it would allow generic algorithms to be written for integers, floating points and possible future complex numbers, but that's about it. The name is purposefully vague.
If you want a strict mathematical grouping of numbers, I think it's better to introduce a different type or protocol, in its own library.
Right, it's an explicit non-goal of the numeric protocols to closely mirror the usual lattice of mathematical algebraic abstraction. Numeric corresponds roughly to Ring, which is deliberate because it's a pretty good match for our intuition of "numbery thing", but beyond that it's likely a mistake to try to match protocols with mathematical abstraction--they are tied not only to the abstract object being modeled, but also to some implementation choices of that modeling.
As a concrete example, FloatingPoint is not just "approximate Reals"; it also implies IEEE 754 operations and semantics. If someone were to implement Unum or ComputableReal or whatever, it won't conform to FloatingPoint and that will be OK.
Even with Numeric, there is a magnitude requirement which doesn’t make sense for rings in general. So yeah, pretty much no correspondence between protocols and algebraic structures whatsoever.
I feel that Complex numbers should work for any real type (SignedNumeric & Comparable), not just FloatingPoint. There would be slightly different interfaces because of the division interface.
I also imagine them being implemented as a strong type alias to a two-element fixed-sized array. C++ has memory layout rules for complex to allow aliasing like this. Reading that inspired the designs for both that I’ve mentioned here.
As the names suggest, types comforming to Real are intended to model the set of real numbers. They support elementary functions such as sine and cosine. By the same token, Complex is intended to model the set of complex numbers; Complex relies on elementary functions defined on T to support many of its own methods.
You can, of course, define a struct with two stored integers to model Gaussian integers, but that'd be a different type in almost every other respect.
“Complex” Integers (Gaussian Integers), while a subring of the Complex numbers, are in practice used pretty differently. It’s not obvious that sharing a generic type for them buys you much—there’s not a lot of code that you want to be generic over these two objects.
Furthermore, since a ComplexInt ought to conform to Numeric, it can’t even be generic over integer types, because of the magnitude requirement. I suppose one could resort to shenanigans like making the magnitude always be a Double regardless of the underlying integer type, but that seems…unprincipled.
So I think @xwu has the right approach. Though I’d probably start with Complex<T: FloatingPoint>, and add things like trig functions in a constrained extension.
Most of the libraries I have used give -nan + i inf, which I don't think is unreasonable. If you think of this in polar form you have 1<pi/2 * inf<0 = inf<pi/2. If we then convert back to cartesian form inf cos(pi/2) + i inf sin(pi/2) = inf * 0 + i inf = nan + i inf. The hardware I have used gives -nan for some reason, but apart from the minus the result looks OK to me.
Your post is pretty long, so I'll adress it piece by piece
How does (1) come to fruition? Don't you need to introduce branching there to skip the floating point operations when the Imaginary or Real part are 0? If so, isn't branching slower than FLOPs?
(2) is true, if you're using reference types and nullifying the Imaginary or Real part when they are 0 or haven't been instantiated yet. But to use a reference comes at a price. In particular, you'd incur into:
A pointer's cost. You need a pointer to the reference. Pointers are 64-bit, which effectively duplicates the size of the referenced Double type.
Cache misses: one of the weaknesses of reference types is that the referenced objects won't necessarily be contiguous on memory. Therefore you'll be triggering cache misses left and right when reading these objects. This can be avoided with a Complex struct that contains the Imaginary and Real pieces, but then you won't be able to nullify 0-valued components in order to save memory.