Operator syntax for vector and matrix arithmetic

Yes, I mean multiplication in the matrix ring in the linear algebra sense. This is common in, eg, computer graphics.

I feel like the two disparate use cases might be best served by two different data types, then. One for linear transformations, and another for a 2-dimensional array of values.

I had not thought of using two different data types :thinking:. So what would you name these different types? For the linear algebra stuff it would obviously be Vector and Matrix. But for a bag of values or array of values in 2D what name would you use? Maybe use Tensor?

I'm just spitballing, but another possibility is to name the linear algebra one LinearOperator or Transformation or something, and use Matrix for your element-wise thing.

Using separate data types is what I was suggesting above, then there is no question about the domains of use.

(I'd strongly recommend against using Tensors for general bags of numbers, as they have a very specific meaning in differential geometric contexts.)

1 Like

Well, in matrix code, I rarely if ever do element-wise multiplication between two matrices, so an equation like that wouldn't come up. I could imagine creating a different object which is a bag of real numbers that used * and ^ as you describe, and then ok, bag₁ * bag₂ could be fine to let it be element-wise, but I like to keep these concepts separate.

Oh I see. So you were suggesting something like Vector, Matrix, and Bag structures?

Well, probably not the name Bag per se, which implies totally unstructured storage, but something other than Matrix (and Tensor).

1 Like

Other names that come to mind are Grid, Mesh,Table, Array2D, ShapedArray. NumPy represents numeric data as an ndarray and has no vector or matrix types. Julia has vectors and matrices but they seem to be one-dimensional or N-dimensional versions of the Array type. But Array is already taken in Swift so that's not an option here.

Array2D might be a good choice. This thread actually made me wonder whether Swift should carry have separate MatrixNxN<T> and SIMDNxN<T> types, rather than propagate C’s behavior of aliasing matrix4x4 to simd_float4x4 for the purpose of separating the conceptual operations from the storage format.

3 Likes

If I go with your concept of keeping things separate, I can have Vector and Matrix structs for linear algebra operations, and have a ShapedArray struct for two-dimensional or N-dimensional numeric operations. In the table below, I listed the operators that could be used for each type for various arithmetic operations. The * operator is used for matrix multiplication with Matrix structs and for element-wise multiplication with ShapedArray structs. Is this the approach that you are suggesting?

Arithmetic operators Vector Matrix ShapedArray
element-wise addition + + +
element-wise subtraction - - -
element-wise multiplication *
multiplication *
element-wise division /
element-wise power ^
2 Likes

for matrix multiplication, i’ve long used ><, as it sort of resembles a × sign in ASCII. you can also pair it with the <>, which is also a legal operator lexeme in Swift. i would avoid using Unicode operators.

Since it's multiplication, the standard multiplication symbol * could be included such as <*> or >*<.

One argument in favor of dotted operations — or at least this what Julia got out of them — is that you don't need to overload operations to work on every pair of types. You can define just scalar multiplication, and then get scalar * matrix, matrix * scalar, and matrix elementwise* matrix all for free, just by dotting the operations. (Of course, you do need to define broadcasting per type — but that's just one function per type instead of one per operation per pair of types.) Perhaps it's better to think of the dots as a call to map and zip with the given operation and operands, rather than .* as its own operation.

However perhaps none of this matters because that's not how those operations would work in Swift?

1 Like

I personally would be very confused if using * between two objects that claim to be matrices did anything other than matrix multiplication.

While in some contexts the term matrix is used loosely to talk about "bags" of numbers arranged in rows and columns, there's usually decent naming alternatives for that (ie Numpy's ndarray). However, when matrices are used in linear algebra, there are no good naming alternatives. The term "matrix" is already defined to mean something very specific. For that reason, I would strongly encourage naming the type anything other than Matrix if the * operator is going to do something other than matrix multiplication.

I do like the idea of keeping separate types, like Matrix and ShapedArray to keep the conceptual distinction clear, and have the * operator do different things based on the type.

Also, Vector is an interesting case, as one would need to support both row-vectors and column-vectors, and at that point it's just a special case of Matrix. IIRC Numpy sidesteps this issue by calling everything an array.


We're totally looking at this from different perspectives, but I wouldn't have considered whether the operation involves doing some computations element-wise or not is not as an important distinction when dealing with matrices.

To me, the distinction is whether or not the operation is (traditionally) defined for matrices:

  • Addition, subtraction and (matrix) multiplication are well defined, so I'd use the plain operators +, - and *, because that's the default meaning of addition, subtraction and multiplication when dealing with matrices.
  • Element-wise multiplication, division, exponentials... are the "extraneous" operations for matrices (that only make sense when treating matrices as bags of numbers), so I'd use different operators for those: .*, ./, .^ (for example).

So having +, - and * is already consistent in some way, even though the first two do "element-wise" computations and the third doesn't.

This would change if the objects had a different name that didn't imply that the object is a matrix (ie ShapedArray). Then I might have defaulted to * for element-wise multiplication and used something different (maybe even a more explicit matmul() method) for matrix multiplication.

One downside of LinearOperator is that matrices are often used to perform non-linear operations. Notably in graphics, for example: perspective projection matrices.

It feels wrong to write:

let projectionMatrix: LinearOperator4x4

Because the perspective projection is a non-linear transformation in 3D (it's still a linear operator on the 4D vector, but in this case you're only interested in the first 3 coordinates, which makes the LinearOperator naming a bit awkward).

4 Likes

Thank you for the in-depth reply. I definitely need to consider using different types such as Vector, Matrix, and ShapedArray and implement the appropriate operations for those types. Using a Matrix struct as both a linear algebra object and as a bag/collection of numbers just leads to confusion as many here have pointed out.

1 Like

A little late to the discussion, but I have a slightly slanted use case from all those I’ve seen so far. I’m designing a framework for interactive applications to compute and display Geometric Algebra objects and transformations on them. I’d like to use operator notation that’s standard in this domain (much of it going back to the 1960s and 70s), such that it can run in a Swift Playground book without my having to build yet another parser to convert the notation to function names.

Note that the objects of interest here are scalars (can be real, complex or even hypercomplex) and multivectors of arbitrary dimension, and the transformations are multivectors.

There are 3 different multiplication operators in this domain: interior (inner or dot) product, exterior product, and geometric product. Exterior product (usually called “wedge”) has been universally notated as ‘^’ for at least 40 years, and I’m afraid that’s (pun intended) firmly wedged in everyone’s mind. I would like to use ‘*’ for the geometric product, as that’s fairly common usage. Inner product is more of a challenge, and mathematicians and programmers have resorted to, for instance’ treating it as ‘left contraction’ and using ‘[‘ or ‘⎣’. I would prefer using ‘⦁’, though I know the arguments against using arbitrary Unicode characters. But there are ways to bind multi-key combinations to Unicode, so I think it’s reasonable to use characters familiar to domain users.

There are also a number of other operators that are not likely to be controversial, such as “perpendicular”, “parallel”, “plunge”, and I expect I will use Unicode characters for these as well.

3 Likes

Using Unicode characters to represent mathematical operations is completely reasonable in my opinion. I have no problems using symbols from Unicode as function definitions. You can always provide an equivalent named function if people don't want to use the Unicode version. I would like to mention that the Julia language supports Unicode input such as \pm for ± and \degree for °.