Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift Generics: IntegerType Version Works But Not FloatingPointType

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?

like image 858
xBACP Avatar asked Dec 24 '15 22:12

xBACP


1 Answers

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.

like image 87
dfrib Avatar answered Nov 17 '22 11:11

dfrib