I'm working on a linear algebra package that relies on Accelerate, BLAS, and LAPACK for performing vector and matrix operations. Consequently, most of the operations use functions that are only applicable to a certain value type such as Float or Double. This makes for a lot of redundant code and documentation because I need to provide documentation for each function for each specific value type. The example below demonstrates this.
Example without a protocol
Here is a generic vector structure that uses a flat array as the underlying value storage.
// Vector.swift
struct Vector<T> {
let size: Int
var values: [T]
init(_ values: [T]) {
self.size = values.count
self.values = values
}
init(like vector: Self) {
self.size = vector.size
self.values = vector.values
}
}
The code below uses the vector struct to perform element-wise vector addition for Int, Float, and Double values. The Accelerate vDSP.add function only supports Float and Double values so a loop is used for vectors that contain Int values. Basically, the same function has been defined for each value type that I want to support. The documentation comments for each function are also similar. This is a lot of duplicate code which generates a lot of redundant documentation. I would like to have just one function that handles the different value types and therefore one function with documentation comments.
// Addition.swift
import Accelerate
/// Add two vectors using integer values.
/// - Parameters:
/// - a: The first vector.
/// - b: The second vector.
/// - Returns: The result vector.
func add(_ a: Vector<Int>, _ b: Vector<Int>) -> Vector<Int> {
var result = [Int](repeating: 0, count: a.size)
for i in 0..<a.size {
result[i] = a.values[i] + b.values[i]
}
return Vector(result)
}
/// Add two vectors using single-precision values.
/// - Parameters:
/// - a: The first vector.
/// - b: The second vector.
/// - Returns: The result vector.
func add(_ a: Vector<Float>, _ b: Vector<Float>) -> Vector<Float> {
var vec = Vector(like: a)
vDSP.add(a.values, b.values, result: &vec.values)
return vec
}
/// Add two vectors using double-precision values.
/// - Parameters:
/// - a: The first vector.
/// - b: The second vector.
/// - Returns: The result vector.
func add(_ a: Vector<Double>, _ b: Vector<Double>) -> Vector<Double> {
var vec = Vector(like: a)
vDSP.add(a.values, b.values, result: &vec.values)
return vec
}
Usage of this code is shown below along with the print output.
let a = Vector([1, 2, 3]) // integer
let b = Vector([9, 3, 4])
let c = add(a, b)
print(c)
let a1 = Vector<Float>([1, 2, 3]) // float
let b1 = Vector<Float>([9, 3, 4])
let c1 = add(a1, b1)
print(c1)
let a2 = Vector([1.9, 2, 3]) // double
let b2 = Vector([9, 3.8, 4])
let c2 = add(a2, b2)
print(c2)
Vector<Int>(size: 3, values: [10, 5, 7])
Vector<Float>(size: 3, values: [10.0, 5.0, 7.0])
Vector<Double>(size: 3, values: [10.9, 5.8, 7.0])
Example with protocol
The only solution (that I know of) to reduce redundant code and documentation is to use a protocol along with generic values. Using the same vector struct defined above, I can define a scalar protocol as follows:
// Scalar.swift
protocol Scalar {
static func add(_ a: Vector<Self>, _ b: Vector<Self>) -> Vector<Self>
}
Using this protocol I can extend the Int, Float, and Double types with the add function for that particular type. See below for the Float and Double extensions.
// Float+Scalar.swift
import Accelerate
extension Float: Scalar {
static func add(_ a: Vector<Self>, _ b: Vector<Self>) -> Vector<Self> {
var vec = Vector(like: a)
vDSP.add(a.values, b.values, result: &vec.values)
return vec
}
}
// Double+Scalar.swift
import Accelerate
extension Double: Scalar {
static func add(_ a: Vector<Self>, _ b: Vector<Self>) -> Vector<Self> {
var vec = Vector(like: a)
vDSP.add(a.values, b.values, result: &vec.values)
return vec
}
}
This allows me to have one public add function and therefore one set of documentation comments which applies to all the supported types. See the add function below.
// Addition.swift
/// Element-wise addition of two vectors.
/// - Parameters:
/// - a: The first vector.
/// - b: The second vector.
/// - Returns: The result of adding two vectors.
func add<T: Scalar>(_ a: Vector<T>, _ b: Vector<T>) -> Vector<T> {
T.add(a, b)
}
Usage of the above code and the print output is shown below.
let a = Vector<Float>([1, 2, 3, 4])
let b = Vector<Float>([4, 8, 5, 10])
let c = add(a, b)
print(c)
let a1 = Vector([1, 2, 3, 4.0])
let b1 = Vector<([4, 8, 5, 10.0])
let c1 = add(a1, b1)
print(c1)
Vector<Float>(size: 4, values: [5.0, 10.0, 8.0, 14.0])
Vector<Double>(size: 4, values: [5.0, 10.0, 8.0, 14.0])
This approach requires more internal code but the amount of public functions and documentation is greatly reduced.
Questions
I have several questions related to all of this:
- Is using a protocol a good approach to reduce duplicate public code and documentation for this situation?
- If I don't use the protocol approach, does DocC provide any features to produce one set of documentation for functions that handle different value types?
- Are there any performance issues that I should be aware of when using the protocol approach compared to the non-protocol approach?
- Is there a better name other than Scalar that I can use for the protocol implemented above? I thought about calling it Numeric but Swift already has a Numeric protocol.
- Does the generic element in the vector struct need to conform to the protocol? For example, should I use
struct Vector<T: Scalar> { ... }
instead ofstruct Vector<T> { ... }
?