I typed up two version of an algorithm for discrete convolution using Swift generics. The integer version works. But the floating point version has an issue with multiplication:
import Foundation
import Swift
func linconv<T: IntegerType>(signal_A signal_A: [T], signal_B: [T]) -> [T]? {
// guard
guard signal_A.isEmpty == false && signal_B.isEmpty == false else {
return nil
}
// reverse at least one of the arrays
//let signal_A_reversed = Array(signal_A.reverse())
// size of new array
let N = signal_A.count + signal_B.count - 1
// new array for result
var resultSignal = [T](count: N, repeatedValue: 0)
for n in 0..<N {
for j in 0...n {
if j < signal_B.count && (n - j) < signal_A.count {
resultSignal[n] += signal_B[j] * signal_A[n - j]
}
}
}
return resultSignal
}
func linconv<T: FloatingPointType>(signal_A signal_A: [T], signal_B: [T]) -> [T]? {
// guard
guard signal_A.isEmpty == false && signal_B.isEmpty == false else {
return nil
}
// reverse at least one of the arrays
//let signal_A_reversed = Array(signal_A.reverse())
// size of new array
let N = signal_A.count + signal_B.count - 1
// new array for result
var resultSignal = [T](count: N, repeatedValue: T(0))
for n in 0..<N {
for j in 0...n {
if j < signal_B.count && (n - j) < signal_A.count {
resultSignal[n] += signal_B[j] * signal_A[n - j] // compiler says error here!
}
}
}
return resultSignal
}
For the FloatingPointType version, the compiler says "Binary operator '*' cannot be applied to two 'T' operands". But, it does not do this on the IntegerType version. Why?
The FloatingPointType
protocol is indeed adopted by Double
and Float
types, but conversely, the protocol, for some reason, does not include blueprints for operator methods such as (in your case), *
binary operator or +=
assignment operator. Note here the importance that just because some known types adopt a protocol, that protocol in itself must not necessarily contain all the blueprints we would expect for those types that have adopted it.
The IntegerType
protocol, on the other hand, does include blueprints for the operator methods.
Hence, your generic T
conforming to protocol FloatingPointType
is not necessarily (in the eyes of swift) multiplicable and so on, as the protocol includes no blueprints for such operations. If we look at the standard library reference for FloatingPointType, we see that it is seemingly only adopted by Double
, Float
(and CGFloat
). We know all these three types work well, on their own, with our regular operators, so hey, why can't we use those operators on a generic conforming to FloatingPointType
? Again, the collection of types that are conforming to a protocol really gives no insight into what blueprints that protocol contains.
As an example, look at the following protocol the extension of some fundamental types to conform to it
protocol ReallyLotsOfAdditionalStuff {}
extension Int : ReallyLotsOfAdditionalStuff {}
extension Double : ReallyLotsOfAdditionalStuff {}
The library reference for this dummy protocol would list that only types Int
and Double
adopt it. Conversely, if we are not careful, we could expect that generics conforming to protocol ReallyLotsOfAdditionalStuff
would at least be say, addable (in addition to lots of additional stuff), but naturally, this is not the case.
Anyway, you can fix this yourself, however, by creating a new protocol that you add as an additional type constraint for the generic T
in your FloatingPointType
function.
protocol MyNecessaryFloatingPointTypeOperations {
func *(lhs: Self, rhs: Self) -> Self
func += (inout lhs: Self, rhs: Self)
// ... other necessary floating point operator blueprints ...
}
extension Float: MyNecessaryFloatingPointTypeOperations {}
extension Double: MyNecessaryFloatingPointTypeOperations {}
// Example: only type constraint to FloatingPointType
func errorFloatingPointType<T: FloatingPointType> (a: T, b: T) -> T {
return a * b // Error: binary operator '*' cannot be applied to two 'T' operands
}
// Example: additional type constraint to your custom protocol
func noErrorFloatingPointType<T: protocol<FloatingPointType, MyNecessaryFloatingPointTypeOperations>> (a: T, b: T) -> T {
return a * b // ok!
}
Hence, to fix your FloatingPointType
, add your custom protocol as an additional type constraint for T
in the function header:
func linconv<T: protocol<FloatingPointType, MyNecessaryFloatingPointTypeOperations>>(signal_A: [T], signal_B: [T]) -> [T]? {
// ...
}
Alternatively, let your own protocol inherit FloatingPointType
and add any additional methods needed to your "child protocol", e.g.:
protocol ImprovedFloatingPointType : FloatingPointType {
func *(lhs: Self, rhs: Self) -> Self
func += (inout lhs: Self, rhs: Self)
// ... other necessary integer and floating point blueprints
}
extension Float: ImprovedFloatingPointType {}
extension Double: ImprovedFloatingPointType {}
func linconv<T: ImprovedFloatingPointType>(signal_A: [T], signal_B: [T]) -> [T]? {
// ...
}
Finally, we might ask, do we even need the FloatingPointType
protocol in the first place (even as parent protocol to our custom one)? If we only want to make a generic for handling the two swift floating point types Double
and Float
, then we might as well apply only protocol MyNecessaryFloatingPointTypeOperations
as a type constraint to our generic:
func myFloatingPointGenericFunction<T: MyNecessaryFloatingPointTypeOperations> (a: T, b: T) -> T {
// ...
return a * b
}
As you might already know, we need the generic to conform to FloatingPointType
protocol for a single blueprint: to ascertain our generic function that we can initialise T
instances using an integer initialiser, namely init(_ value: Int)
. E.g., in your function:
// new array for result
var resultSignal = [T](count: N, repeatedValue: T(0)) // <--
However, if this is the only blueprint we're using from the FloatingPointType
protocol, we might as well add it as a blueprint to our own protocol instead, and remove the generic type constraint to FloatingPointType
entirely.
protocol MyNecessaryFloatingPointTypeOperations {
func *(lhs: Self, rhs: Self) -> Self
func += (inout lhs: Self, rhs: Self)
init(_ value: Int)
// ... other necessary floating point blueprints
}
extension Float: MyNecessaryFloatingPointTypeOperations {}
extension Double: MyNecessaryFloatingPointTypeOperations {}
func myFloatingPointGenericFunction<T: MyNecessaryFloatingPointTypeOperations> (a: T, b: T) -> T {
// ...
var c = T(0) // OK
c += a * b // OK
return c
}
With this, we realise that we don't really need two separate generics for the integer types and the floating point types. Since we (for your example) only need 1. an by-int-initializer, 2. * binary operator, and 3. += assignment operator, we could construct a generic for all types that conform to these blue marks as a type constraint, and extend the types that we wish to be covered by our generic by this protocol.
protocol MyCustomProtocol {
func *(lhs: Self, rhs: Self) -> Self
func += (inout lhs: Self, rhs: Self)
init(_ value: Int)
// ... other necessary integer and floating point blueprints
}
extension Int: MyCustomProtocol {}
extension Float: MyCustomProtocol {}
extension Double: MyCustomProtocol {}
func myIntAndFloatGenericFunction<T: MyCustomProtocol> (a: T, _ b: T) -> T {
// ...
var c = T(0) // OK
c += a * b // OK
return c
}
let aInt = 2
let bInt = 3
let aInt32: Int32 = 2
let bInt32: Int32 = 3
let aDouble = 2.5
let bDouble = 3.0
let cInt = myIntAndFloatGenericFunction(aInt, bInt) // 6
let cInt32 = myIntAndFloatGenericFunction(aInt32, bInt32) // error
let cDouble = myIntAndFloatGenericFunction(aDouble, bDouble) // 7.5
Here, however, we see one gain of using the existing IntegerType
protocol: it is already adopted by numerous integer types, whereas for our custom protocol, all of these int types (if we want to use them in our generic) needs to be explicitly extended to adopt our custom protocol.
To wrap it up: if you know explicitly which types you want to be covered by your generic, you might swell write your own custom protocol and extend these types to adapt to this. If you want all (numerous) different integer types, using two separate generic for ints and floats, with protocol IntegerType
for the latter, is probably to prefer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With