2
votes

I'm having trouble getting Swift to understand that an array of objects that conform to two protocols is the same as an array that conforms to one of them.

Let's say I have two protocols, Identifiable and Namable:

protocol Identifiable {
    var identifier: Int { get }
}

protocol Namable {
    var name: String { get }
}

And two functions that will print info about arrays of objects conforming to those protocols:

func printIdentifiers(itemsToPrint: [Identifiable]) {
    for (itemNumber, item) in itemsToPrint.enumerate() {
        print("\(itemNumber): \(item.identifier)")
    }
}

func printNames(itemsToPrint: [Namable]) {
    for (itemNumber, item) in itemsToPrint.enumerate() {
        print("\(itemNumber): \(item.name)")
    }
}

And then two structs that conform to those protocols:

struct Friend: Identifiable, Namable {
    var identifier: Int
    var name: String
}

struct Dog: Identifiable, Namable {
    var identifier: Int
    var name: String
}

And then say I have an array of items that conform to both those protocols:

let jeff = Friend(identifier: 232314, name: "Jeff")
let fido = Dog(identifier: 45678, name: "Fido")
let identifiableAndNamableItems: [protocol<Identifiable, Namable>] = [jeff, fido]

Swift has no problem when I assign jeff to a variable that's Namable:

let namableJeff: Namable = jeff //This is fine!

But it freaks out when I try to do:

printNames(identifiableAndNamableItems)

Cannot convert value of type [protocol<Identifiable, Namable>] to expected argument type [Namable]

Any idea why? Swift knows intuitively that a variable with type protocol<Identifiable, Namable> can be assigned to a variable with type Namable, since any object that conforms to two protocols must necessarily conform to just one of the protocols. But it does not understand that an array of items that conform to two protocols can be assigned to an array of items that conform to one of the protocols.

2
"But it does not understand that an array of items that conform to two protocols can be assigned to an array of items that conform to one of the protocols" Correct, because "array-of-items-that-conform-to-a-protocol" is not actually a full-fledged type. See the discussion of my question here: stackoverflow.com/questions/33112559/…matt

2 Answers

1
votes

Swift cant perform a full collection type conversion (only available for some behind-the-hood automatically Objective-C-bridgeable objects, or between collections of super- and subclass elements) where the elements of the collection themselves are associated in the sense that one can be assigned to the other. You need to explicitly help out the compiler to show that element-by-element conversion is valid, e.g. using a .map operation prior to calling printNames

printNames(identifiableAndNamableItems.map{ $0 })
    /* 0: Jeff
       1: Fido */

Note also that you needn't go all out with multiple protocols to see this behaviour; it is likewise apparent for e.g. the following more minimal example

protocol Foo { }
struct Bar: Foo {}

let bar = Bar()
let foo: Foo = bar // ok

let barArr: [Bar] = [Bar(), Bar()]
let fooArr: [Foo] = barArr // cannot convert value of type '[Bar]' to specified type '[Foo]'
// let fooArr: [Foo] = barArr.map{ $0 } // OK
1
votes

Both @dfri and @matt make great points about why this doesn't work. It's a combination of the fact that implicit collection type conversion is extremely limited, and Swift hates using non-concrete types in most circumstances.

The only thing I would add is that a slightly more concrete solution (pun intended) to the problem than having to use map to manually convert types, is to use a type erasure, as Rob demonstrates in his answer here.

This will allow you to wrap your non-concrete type of protocol<Identifiable, Namable> in a new concrete type AnyIdentifiableAndNamable (feel free to come up with a more catchy name). You can then use this concrete type for your array.

You'll want it to look something like this:

struct AnyIdentifiableAndNamable:Identifiable, Namable {

    // your non-concrete typed base
    private let _base:protocol<Identifiable, Namable>

    // implement protocol properties to simply return the base's property
    var identifier: Int {return _base.identifier}
    var name: String {return _base.name}

    init<T:Identifiable where T:Namable>(_ base:T) {
        _base = base
    }
}

let jeff = Friend(identifier: 232314, name: "Jeff")
let fido = Dog(identifier: 45678, name: "Fido")
let identifiableAndNamableItems = [AnyIdentifiableAndNamable(jeff), AnyIdentifiableAndNamable(fido)]

You then just have to modify your print functions to use generics. For example:

func printIdentifiers<T:Identifiable>(itemsToPrint: [T]) {
    for (itemNumber, item) in itemsToPrint.enumerate() {
        print("\(itemNumber): \(item.identifier)")
    }
}

func printNames<T:Namable>(itemsToPrint: [T]) {
    for (itemNumber, item) in itemsToPrint.enumerate() {
        print("\(itemNumber): \(item.name)")
    }
}

Now you don't have to perform any conversions to pass your [AnyIdentifiableAndNamable] to [T], as Swift will infer the type for you.