Generic "math functions"

What would the advantage of such a system be vs just working with a Complex type? Every result would necessarily be at least as large as a Complex (because it has to be able to accomodate Complex values), and it wouldn’t gain you any typesafety because you would need to have the full complement of heterogeneous operations. It wouldn’t even gain performance because it would need to branch on the discriminator for every operation in most contexts.

Simply using a Complex type is better in basically every way, except maybe for compact representation of inputs known at compile-time to be real, depending on the optimizer (but even there you could capture the same optimization with an explicit conversion).

2 Likes

There is no branching as long as you keep track of the types. Check out this protocol. Implementing that (as I've done in objective-c) actually allows you to avoid branching.

Think of it this way: multiplying two complex numbers, each with real and imaginary component, requires 6 floating point operations (FLOPs). However, multiplication of mixed types, often requires far fewer operations. For example, multiplication of two real numbers or two imaginary numbers requires only 1 FLOP---and an imaginary number multiplying a complex number requires only 2 FLOPs.

As a practical example, one method for differentiating a function is called spectral differentiation which requires a discrete Fourier tranform (DFT), a multiplication, and an inverse DFT. The DFTs, themselves, are complex linear transformations (a matrix multiplication). In practice, the DFT is implemented using an fast Fourier transform (FFT) algorithm. In any 'spectral model', FFTs and complex multiplication are repeated in a for-loop as the model time-steps forward in time. The differentiation itself is the the multiplication of sqrt(-1)2pi*f_n by the complex vector x_n. If the vector is length N, then the optimal number of operations is 2 N FLOPs.

A numerical complex number system should therefore avoid unnecessarily defaulting to the most general case of 6 FLOPs.

Also, because the types are kept track of, you reduce memory requirements.

First: you have already answered your "what do we call these?" question--the names are right there in your implementation.

Second, for simple scalar computations, yes, this will mostly let you elide branches and avoid some space. But if I have an array of these things, some of the elements in that array may be real, and some of them may be complex. I have two reasonable options:

  • promote all of them up to complex, do some extra arithmetic, but have completely uniform access patterns and no branching.
  • make a side array of per-element discriminators, use that to do complex indexing arithmetic and branch around the implementations.

FLOPs are cheap. Memory indexing is expensive. Branching is super-expensive. There are specific constrained situations where it can be a win to do this sort of thing (mostly if I know a priori that every element of an array is real or complex--but then I could just as easily use Array<RealType> or Array<Complex> directly and use an explicit conversion precisely where needed), but more generally I have not seen a convincing demonstration.

1 Like

There's a bit of a name clash between the Real protocol that you designed and the Real protocol that I suggested that sits on top of that.

So, in my modeling framework that I linked to, the reason that it works and is fast in objective-c is that I am using big arrays of these things. In my example I gave above, those arrays contain O(1e6) values in them. And, yes, they're all all imaginary, all complex, or all real.

Obviously you're not modeling fluid flows like I am... yes FLOPs are cheap, but a factor of three in speed is a big deal for me.

Which is why it's great keeping the array sizes down but not automatically assuming everything is complex.

Again, which is why this system is great. No branching necessary!

It sounds like you don't do numerical modeling, or do spectral analysis of data all day, and therefore might not see the need for this, but please recognize that there are others that do.

I'm trying to help the Swift community see some of the bigger picture of what other communities need and want with a Math library. Swift can have a number system that works for scientists and engineers, but also keeps speed and types explicit (unlike Matlab).

But anyway, if you do need examples, I have multiple demonstrations of this working just fine. Just one of many examples, but here is the code to model the quasigeostrophic potential vorticity equation. You write down to differential operators just like you would on paper, and then the framework can build the big arrays of real, imaginary, or complex types and do the appropriate operations. The point is that an end user doesn't have to keep track of these things. These fundamental operations (like adding a real and imaginary number) get coded once, so fewer mistakes are made.

To answer this question, one should think of concatenation of arrays in the same way as any other binary operation (like addition). In other words, add(Real,Imaginary)->Complex and similarly concat(Real,Imaginary)->Complex. So these get coded once, and is a perfectly deterministic operation with no branching.

There are some cases where you might want to walk through each element in an array and check it's type, like importing data. But once that's done, there's no branching necessary (using the basic operations).

FYI the recent forum update appears to interpret generic parameters as HTML tags, which then disappear from the post.

let x: Array<Real> = [1, -1, 2]
let y = sqrt(x)

what is the type of y?

If it's Array<Complex>, I would achieve the same thing, while being significantly less surprising (and much more in line with the way the rest of the Swift language behaves), by requiring an explicit conversion:

let y = sqrt(Array<Complex>(x))

If your complaint is just that you don't like this explicit conversion--that's a pattern that's really baked in throughout the standard library. There's some magic around optionals and in a few other areas, but we really don't want to add more of that magic. Developers can certainly add a function that does the conversion and square root in their own projects if necessary, but we probably wouldn't take it in the stdlib.

If it's some other type, what is it?

2 Likes

Correct, it would go to Array<Complex>.

Yes, I agree that it would be different than the way the language currently behaves (and some additional language changes might be necessary), but I think it would significantly less surprising for most people (per my comment here).

Swift currently advertises itself as being type-safe, and I'm also arguing for math-safe. The types of programming where it's important to have y = [1,.nan,1.4] are certainly more of an edge case than the types of programming where y = [1,i,1.4]. So if you need the edge-case behavior, then stick to some Float type that promises that. So I think there's a good argument that a math-safe behavior should be the default... but sure, perhaps something like this doesn't go in the stdlib.

Ok, follow-up: what if the values are [1, 2, 3]?

2 Likes

That is not the expected behavior for Swift users. As the vast majority of statically typed languages (and even SQL does this) operations between number types return the same number type.

Operations between Int should return Int and operations between Float should return Float. In Swift 1 / 2 returns Int 0 even though it is not mathematically correct because it is the expected and correct behavior.

5 Likes

As I indicated in the protocol I sketched and linked to, those would stay positive real.

There are few tweaks to parsing string literals required, but it's all possible.

Absolutely. Let Int and Float do what they currently do. Let something like Integer and Real do different.

Wait, what?

var x: Array<Real> = [1, 2, 3]
var y = sqrt(x) // what is the type of `y`?
x = [1, -1, 2]
y = sqrt(x) // what is the type of `y`?

That is inconsistent with what's already in Swift. Int conforms to the *Integer protocols, just as it is proposed here that Float conform to Real.

1 Like

This is in protocol I linked to above. Here it is again.

The basic idea is that you can create PositiveReal and NegativeReal as inherited protocols of Real... so as long as the type of your array is PositiveReal and not just Real, it handles the edge case you're eluding to.

String literal parsing would have to be able to distinguish between the signed version and unsigned version... but this is possible. I have some ideas about how to do this.

There are actually a few other things that would need to change in the language...but I didn't want to wade into those details yet.

My problem with this is, IIUC:

> let x = PositiveReal(4)
> sqrt(x)
Real(2)
> sqrt(x + 1 - 1)
Complex(2)

This is far, far weirder to me than sqrt(-1) = nan.

2 Likes

*Integer and Real are the protocols and Int*, Float/Double are the concrete types. I don't think magic Integer to Real and Real to Complex operators are needed in the Swift standard library. If you want to work with integers, reals and complex you would start with Complex values and all operators would already return Complex.

That's basically correct, except in the first case sqrt(x) results in a PositiveReal.

And yes, in the second case you get a Complex out because subtracting a PositiveReal from PositiveReal can only guarantee a Real, the square root of which results in Complex.

But, I would say that's a helpful result, isn't it? The compiler was able to tell you that you did a series of mathematical operations that result in something that can't be guaranteed to be positive real. If that contrary to an assumption you were making, you can go chase that down.

That's actually a good example for why I think this idea would be good in Swift and substantially reduce bugs in mathematical programming. One of Swift's strengths, in my opinion, is that I know at compile time the type of each variable... so things are very predictable. The beautiful thing here is that same type-safety gets applied to mathematical operations.

This sort of fine-grained algebraic type system has been attempted a few times, but I haven't seen evidence that it's ever been particularly successful--it's certainly never caught on in the mainstream. My suspicion is that most non-trivial computations in such a system end up with type Complex anyway--even if every value turns out to be real at runtime--so the extra complexity in the type system buys you very little actual type safety.

I've built limited versions of such systems myself in the past (Quaternion and UnitQuaternion, etc) and my experience is that real use escapes the system too quickly for it to have much value.

It's fundamentally very different from the sort of type inference that Swift does today, so I think we would want to see a fleshed-out model that was fairly widely-used by developers before we started designing the standard library around it.

There's also a significant ambiguity in how such heterogeneous arithmetic interacts with literals and the current typechecker:

let x = Complex(0) * 1

Is this:

  • Complex(0) * PositiveReal(1)
  • Complex(0) * Real(1)
  • Complex(0) * Complex(1)

The typechecker has no way to know which of these overloads to choose, so the expression is ambiguous and fails to compile, which is doubly frustrating because the distinction is only syntactic; semantically, these are equivalent! In the long-term we may be able to improve these cases, but it's very hard to imagine building such a system until we have a solution in hand.

2 Likes

Matlab basically does a variation of this and ranks higher in the TIOBE index... so the basic idea has definitely caught on.

I think it's a valid concern that non-trivial computation end up with type Complex anyway. With the caveat that Matlab is doing things differently than proposed here, we (in the scientific community) look at the variable types output in Matlab all the time. It doesn't happen as often as you think---and getting a complex back when you expected a double is super helpful.

Yeah, I'd like to go further---but I need help! The problem with this kind of cross-disciplinary problem is that it requires experts from both fields working together toward a common goal.

Matlab does it dynamically at runtime. That works fine, because you have more information available. My concern is specifically about doing it at compile time in a type system.

Going back to my simple example, if we imagine choosing types at runtime instead:

> let x = PositiveReal(4)
> sqrt(x + 1 - 1) // x + 1 - 1 = 4 > 0, so argument is a PostiiveReal
PositiveReal(2)

Again, it doesn't happen because more information is available at runtime. If you want to build this system based on runtime discrimination, it'll work great, you won't have any trouble, but it will also be more efficient to just bite the bullet and use Complex unless your arrays are quite large.[1]

[1] Here I mean "quite large" in the programmer sense, not in the computational mathematics sense. A few hundred-a thousand, which is absolutely tiny for computational mathematics.

2 Likes