Thanks to Swift’s type safety and generics it is possible to define matrix types with safe operations. Where, for instance, you may only multiply an M×N matrix by a P×Q matrix if N is equal to P.
In this article I tackle this problem in a very generic way, so that you can add type safety to your favourite existing matrix implementation. This is useful since different matrix libraries may perform better in different applications (machine learning algorithms vs 3D graphics for example).
I have embedded all the necessary code in this article (except for a couple of stubbed methods near the end). The main tool’s code is in SafeMatrix.swift. My rudimentary sample implementation of a matrix type is in Matrix.swift, which could be replaced by any existing matrix library. The preparation of that Matrix type and the matrix dimension types are in MatrixSetup.swift and NumeralsSetup.swift. The sample code is in Usage.swift.
The Tool: Safe Matrix Protocol And Type
You may decide to explore the SafeMatrix.swift code beforehand or simply skip to the Setup and Usage sections below.
Setup Part I: Existing Matrix Implementation
As I explained at the start, we need to begin with an existing matrix library. My sample implementation of the Matrix structure below includes initialization, entry lookup, mutable entries, addition, multiplication, and scalar multiplication. In this implementation the entries are of type Double, but they could be Int or any other type. This implementation is missing transposes, determinants, inverses, and many other standard matrix operations.
After we find (or create) our preferred matrix type implementation, we need to make it conform to UnsafeMatrixProtocol. Conformance toUnsafeMatrixProtocol simply requires subscripting and a dimensions:MatrixDimensions getter:
Setup Part II: Numerals
In the sections that follow, we are going to initialize and manipulate instances of the SafeMatrix generic structure. This structure has three associated types;
- MatrixType, conforming to UnsafeMatrixProtocol
- Rows, conforming to NumeralProtocol
- Columns, conforming to NumeralProtocol
As you may have guessed, MatrixType is going to be our “existing” Matrixtype.
So what are Rows and Columns? These are types whose sole purpose is to provide the number of rows and columns of the specialized SafeMatrixstructure. This is so that — among other things — two matrices with different dimensions are always of different type.
In our example we assume that we have two global variables m and n, and that our code will feature m×n, n×m, m×m, and n×n matrices.
In real-world use cases you may need these values to be loaded from an external source such as a file or a web service. I am not considering those cases here, but it is possible to tackle them with a modified setup.
Usage Part I: Initialization
I am finally ready to show you the resulting m×n safe matrix type. This is simply SafeMatrix<Matrix,M,N>, and can be initialized with a m×nMatrix.
This line of code is too long and hard to read, an issue that we can fix by conveniently extending SafeMatrixProtocol in our MatrixSetup.swift file:
Observe that we use Self.staticDimensions rather than self.dimensionsbecause the latter is not available before initialization. This extension allows us to initialize SafeMatrix<Matrix,M,N> in a much shorter fashion:
Usage Part II: Operators
Recall that our Matrix type supports addition, multiplication, and scalar multiplication operators. We would like to port these over toSafeMatrix<Matrix>. For this we again need to edit MatrixSetup.swift:
Usage Part III: Transpose, Inverse, and Determinant
In this section we assume that the Matrix type contains implementations fortranspose, inverse and determinant methods. For now we add stubbed implementations in Matrix.swift, but be aware that you will need actual implementations for the usageMethods() function to run.
To port these methods over to SafeMatrix<Matrix> types, we need to add two extensions. This is because inverse() and determinant() have a special requirement; Rows == Columns.