Use Accelerate functions with a Matrix struct that has a generic type

I'm trying to make a Matrix type where its values can be Int, Float, or Double. So I defined the Matrix with a generic type named T so it can accept values as Int, Float, or Double (see the example below). However, this approach doesn't work when I use functions from Accelerate like vDSP.add() because the type information for the Matrix values aren't passed to the Accelerate function. Any suggestions on how I should convey the type information to Accelerate functions? I'm new to using generics with Swift so ideas for a better approach are welcome.

import Accelerate

struct Matrix<T> {
    
    let rows: Int
    let columns: Int
    var values: [T]
    
    init(rows: Int, columns: Int, values: [T]) {
        self.rows = rows
        self.columns = columns
        self.values = values
    }
    
    init(rows: Int, columns: Int, fill: T = 0) {
        self.rows = rows
        self.columns = columns
        self.values = Array(repeating: fill, count: rows * columns)
    }
    
    subscript(row: Int, column: Int) -> T {
        get { return values[(row * columns) + column] }
        set { values[(row * columns) + column] = newValue }
    }
    
    static func + (lhs: Matrix, rhs: Matrix) -> Matrix {
        let v = vDSP.add(lhs.values, rhs.values)
        return Matrix(rows: lhs.rows, columns: lhs.columns, values: v)
    }
}
1 Like

You'll need to move that + into an extension which constrains T down to one of the supported types, e.g.

extension Matrix where T == Double {
    static func + (lhs: Matrix, rhs: Matrix) -> Matrix {
        let v = vDSP.add(lhs.values, rhs.values) # Calls https://developer.apple.com/documentation/accelerate/vdsp/3240823-add
        return Matrix(rows: lhs.rows, columns: lhs.columns, values: v)
    }
}

extension Matrix where T == Float {
    static func + (lhs: Matrix, rhs: Matrix) -> Matrix {
        let v = vDSP.add(lhs.values, rhs.values) # Calls https://developer.apple.com/documentation/accelerate/vdsp/3240825-add
        return Matrix(rows: lhs.rows, columns: lhs.columns, values: v)
    }
}

This has the added benefit of making it impossible to call + on matrices of types unsupported by Accelerate (e.g. integers).

Optionally, you can add an unconstrained extension with a catch-all + operator (implemented using manual for loops or whatever), but there's a risk there of having a hidden performance trap in your code, that sneaks by unnoticed.

2 Likes

Ok this works, but it's a lot of duplicate code where the body of each extension is the same. Seems like there would be a cleaner way to do this especially to handle a lot of operations like multiplication, division, subtraction, scalar-to-matrix operations, etc.

I looked at the Accelerate definition for vDSP.add() and it places the where statement with the function definition; similar to what is shown below.

extension Matrix {
    
    func + (lhs: Matrix, rhs: Matrix) -> Matrix where T == Double {
        let v = vDSP.add(lhs.values, rhs.values)
        return Matrix(rows: lhs.rows, columns: lhs.columns, values: v)
    }
}
1 Like

You'll still need some duplication, but you can reduce it if you extract out a helper like with(newValues):

extension Matrix {
    func with(newValues: [T]) -> Matrix {
        Matrix(rows: rows, columns: columns, values: newValues)
    }

    func + (lhs: Matrix, rhs: Matrix) -> Matrix where T == Float {
        with(newValues: vDSP.add(lhs.values, rhs.values))
    }

    func + (lhs: Matrix, rhs: Matrix) -> Matrix where T == Double {
        with(newValues: vDSP.add(lhs.values, rhs.values))
    }

    // ...
}

Then your repeated functions get shorter. Now that it's a expression, so you can omit the return, as well.

1 Like

Shouldn't there be a check that both matrices are of the same dimensions?

1 Like

Yes, dimensions should be checked with a precondition but this question is about handling a generic type; not about checking matrix dimensions. But thanks for the suggestion.

Could you make a macro applied to one version of the method and it produces the other type versions? I haven’t made a macro before, maybe it’ll be as much work as manually duplicating :woman_shrugging:

Natural way to write the above would be:

// not in Swift currently
extension Matrix where T == Float || T == Double {
    static func + (lhs: Matrix, rhs: Matrix) -> Matrix {
        let v = vDSP.add(lhs.values, rhs.values)
        return Matrix(rows: lhs.rows, columns: lhs.columns, values: v)
    }
}

For simple cases like this, this might be nice, but in the general case you end up with the C++ template substitution nightmare scenario. Each call needs to be tried against any of the possible variable bindings, and it gets pretty hard to reason about, pretty quick.

1 Like