fferri
fferri

Reputation: 18940

extend generic Array<T> to adopt protocol

Let's say I've defined such a protocol:

protocol EuclideanPoint {
    func distance(other: Self) -> Double
    func dimension() -> UInt
}

Now I'd like to extend [Float] and [Double] to adopt that protocol.

But the following code:

extension [Float]: EuclideanPoint {
    func distance(other: [Float]) {
        return Double(zip(self, other).map{a, b in pow(a-b,2)}.reduce(0, combine: +))
    }
    func dimension() {
        return UInt(self.count)
    }
}

is invalid because of the error

error: constrained extension must be declared on the unspecialized generic type 'Array' with constraints specified by a 'where' clause

I found similar questions (like this), but the suggested solution is to use extension CollectionType where Generator.Element == S { ... }, but in this context it leads to the error:

error: protocol 'CollectionType' can only be used as a generic constraint because it has Self or associated type requirements

Is there any solution to this?

EDIT:

using the proposed solution:

protocol DoubleConvertibleType {
    var doubleValue: Double { get }
}

extension Double : DoubleConvertibleType { var doubleValue: Double { return self         } }
extension Float  : DoubleConvertibleType { var doubleValue: Double { return Double(self) } }
extension CGFloat: DoubleConvertibleType { var doubleValue: Double { return Double(self) } }

extension Array where Element : DoubleConvertibleType {
    func distance(other: Array) -> Double {
        return Double(zip(self, other).map{ pow($0.0.doubleValue - $0.1.doubleValue, 2) }.reduce(0, combine: +))
    }

    func dimension() -> UInt {
        return UInt(self.count)
    }
}

gives [Double] and [Float] the .distance() and .dimension() methods. Yet [Double] or [Float] cannot be used in place of something that is required to conform to the EuclideanPoint protocol, producing the error:

error: type '[Double]' does not conform to protocol 'EuclideanPoint'

Upvotes: 4

Views: 2354

Answers (3)

dfrib
dfrib

Reputation: 73186

EDITED


The following solution is somewhat generic, conforms to protocol EuclidianPoint, and is based upon two assumptions:

  • That we're allowed to include a generic type constraint for your blueprint for method distance in your EuclideanPoint protocol, and that, instead of argument type being Self, we'll use a generic ([T]). We will, however, ascertain (at compile time) that [T] is of the same type as Self (and here, Self of [Double], [Float] or [Int] type), and ascertain that [T] conforms to the protocol EuclidianPoint.

  • That you're ok that we leave functional programming techniques such as .map and .reduce out of this specific application, and focus only on attaining a "generic array adopted to euclidian protocol". These .map, .reduce etc feats in Swift are indeed neat and useful, but are in many applications just wrappers for behind-the-hood for-loops, so you won't lose any performance over doing things in a manual imperative style. In fact, .reduce is known to perform quite non-optional due to repeated array-copy-assignments while reducing the array (I won't go into this more here...). Anyway, perhaps you can make use of my example and tweak it back to something more functional-paradigmy.


We begin by a custom type protocol, MyTypes, that will act as an interface for which types we want to include in our generic. We also add the slightly updated EuiclidianPoint protocol, where we use protocol MyTypes as a type restraint to the generic T used in the distance (...) function blue-print.

/* Used as type constraint for Generator.Element */
protocol MyTypes {
    func -(lhs: Self, rhs: Self) -> Self
    func +=(inout lhs: Self, rhs: Self)
}

extension Int : MyTypes { }
extension Double : MyTypes { }
extension Float : MyTypes { }
    /* Extend with the types you wish to be covered by the generic ... */

/* Used as extension to Array : blueprints for extension method
to Array where Generator.Element are constrainted to MyTypes */
protocol EuclideanPoint {
    func distance<T: MyTypes> (other: [T]) -> Double?
    func dimension() -> UInt
}

Note that I've changed the Double return of distance to an optional; you may handle this as you will, but in case the lengths of self and other arrays differ, or types Self and [T] differ, there will be some need of showing non-conformance -- I'll use nil for this here.

We can now implement implement our extension of Array by the EuclidianPoint protocol:

/* Array extension by EuclideanPoint protocol */
extension Array : EuclideanPoint {

    func distance<T: MyTypes> (other: [T]) -> Double? {
        /* [T] is Self? proceed, otherwise return nil */
        if let a = self.first {
            if a is T && self.count == other.count {
                var mySum: Double = 0.0
                for (i, sElement) in self.enumerate() {
                    mySum += pow(((sElement as! T) - other[i]) as! Double, 2)
                }
                return sqrt(mySum)
            }
        }
        return nil
    }

    func dimension() -> UInt {
        return UInt(self.count)
    }
}

Note that in the inner if clause of the distance function we use an explicit down cast to T, but since we've asserted that elements of Self are of type T, this is ok.

Anyway, with this, we're done, and we can test our "generic" array extensions, which we note now also conforms to your protocol EuclidianPoint.

/* Tests and Examples */
let arr1d : [Double] = [3.0, 4.0, 0.0]
let arr2d : [Double] = [-3.0, -4.0, 0.0]
let arr3d : [Double] = [-3.0, -4.0]

let arr1f : [Float] = [-3.0, -4.0, 0.0]

let arr1i = [1, 2, 3]

let _a = arr1d.dimension() // 3, OK
let _b = arr1d.distance(arr2d) // 10, OK (A->B dist)
let _c = arr1d.distance(arr1f) // nil (Incomp. types)
let _d = arr1d.distance(arr3d) // nil (Incomp. sizes)
let _e = arr1i.distance(arr1d) // nil (Incomp. types)

    /* for use in function calls: generic array parameters constrained to
       those that conform to protocol 'EuclidianPoint', as requested     */
func bar<T: MyTypes, U: protocol<EuclideanPoint, _ArrayType> where U.Generator.Element == T> (arr1: U, _ arr2: U) -> Double? {

    // ...

    return arr1.distance(Array(arr2))
        /* We'll need to explicitly tell the distance function
           here that we're sending an array, by initializing an 
           array using the Array(..) initializer                */
}
let myDist = bar(arr1d, arr2d) // 10, OK

Ok!


A note still remaining from my first answer:

Extension of generic type Array to protocol was actually just recently asked here:

The consensus is the you cannot do a generic extension of array to a protocol in the "neat swifty" way that you possible expect. There are however workarounds to mimic such a behaviour, one being the one I've used above. If you are interested in another method, I suggest you look into this thread.

Upvotes: 1

luk2302
luk2302

Reputation: 57114

Foreword: As @difri correctly mentions in the comment we can not yet create an extension conforming to a protocol when using generic constrains at the same time. There are already a couple of radars open - searching for "extension of type with constraints cannot have an inheritance clause" will yield a couple of them.

Actual Answer: Building on @LeoDabus awesome answer and my experimenting I came up with the following:

protocol DoubleConvertibleType {
    var doubleValue: Double { get }
}

extension Double : DoubleConvertibleType { var doubleValue: Double { return self         } }
extension Float  : DoubleConvertibleType { var doubleValue: Double { return Double(self) } }
extension CGFloat: DoubleConvertibleType { var doubleValue: Double { return Double(self) } }

extension Array where Element : DoubleConvertibleType {
    func distance(other: Array) -> Double {
        return Double(zip(self, other).map{ pow($0.0.doubleValue - $0.1.doubleValue, 2) }.reduce(0, combine: +))
    }

    func dimension() -> UInt {
        return UInt(self.count)
    }
}

Testing it with

let arr1 = [1.5, 2, 3]
let arr2 = [5.5, 2, 3]
let arrD = arr1.distance(arr2)

Somewhat correctly prints

16

To get the correct answer (at least what I would suspect) you have to wrap the distance into sqrt:

return sqrt(Double(zip(self, other).map{ pow($0.0.doubleValue - $0.1.doubleValue,2) }.reduce(0, combine: +)))

Which then correctly prints

4

Upvotes: 1

Adam
Adam

Reputation: 26917

You can extend SequenceType instead of Array

extension SequenceType where Generator.Element == Float {
//
}

Upvotes: 1

Related Questions