Can't extend Array of elements conforming to Protocol

Suppose I have a protocol:

protocol MSGameTrigger {
    var event: MSGameEvent { get }
    var conditions: [MSGameCondition] { get }
    var actions: [MSGameAction] { get }
}

Which is implemented by >1 structures:

struct MSFlexibleGameTrigger: MSGameTrigger {
    let event: MSGameEvent
    let conditions: [MSGameCondition]
    let actions: [MSGameAction]
}

struct MSWinGameTrigger: MSGameTrigger {
    let event: MSGameEvent
    let conditions: [MSGameCondition]
  
    let actions: [MSGameAction] = [MSWinGameAction()]
}

And there is an array extension:

extension Collection where Element: MSGameTrigger {
    var totalConditions: [MSGameCondition] {
        let conditions = reduce([MSGameCondition]()) { (partialConds, trigger) in
            partialConds + trigger.conditions
        }
        return conditions
    }
}

Now I'm mixing triggers together:

let winTriggers: [MSWinGameTrigger]
let otherTriggers: [MSFlexibleGameTrigger]
let differentTriggers = winTriggers as [MSGameTrigger] + otherTriggers as [MSGameTrigger] 

And I can't apply the extension!

let conditions = differentTriggers.totalConditions

Using 'MSGameTrigger' as a concrete type conforming to protocol 'MSGameTrigger' is not supported

And if edit the extension definition by replacing : with ==, it will not work, in other cases, for example:

let winConditions = winTriggers.totalConditions

Expression type '[MSGameCondition]' is ambiguous without more context

To fix it you need to explicitly cast to more generic type. There is even no ! or ? before as operator, but you still need to cast. I guess it's dumb.

Logically, the Collection where Element: MSGameTrigger extension should work. Any instance of MSGameTrigger has conditions array inside. I see no problem here. Program should be able to iterate through triggers and grab their conditions. The reason why it doesn't work is the fakeness of Swift abilities. On paper it has generics and shit, but in practice it's don't.

P.S.
Its a very common problem of Swift actually. It pretends to be the best possible programming language, I even seen a "research" comparing different languages by features. Swift had almost everything, while other languages like C#, Java, Haskell were behind. But in reality Swift has a lot of dumb practical limitations when you can't implement things which are supposed to be common, like the one in question or an array of weak delegates, for example. This eventually will kill Swift, believe me.

I don't understand the error (it seems like a compiler bug, but I'm hardly qualified to say), but you can do what you want by getting all of the conditions from the separate arrays.

let winTriggers: [MSWinGameTrigger] = []
let winConditions = winTriggers.totalConditions
let otherTriggers: [MSFlexibleGameTrigger] = []
let otherConditions = otherTriggers.totalConditions
let allConditions = winConditions + otherConditions

Also, slow your roll.

1 Like

In Swift, generic requirements with : denote conformance, and protocols do not conform to themselves, which means that MSGameTrigger does not satisfy T where T: MSGameTrigger. This is a well-known intentional aspect of the language and has been extensively discussed multiple times on the forums. The concreteness of the error you mention is evidence of that. While I haven't participated in those discussions, I am sure the team has adequate reasons for why it is this way and ideas of how it could change in the future as the type system matures; all the better if someone reminds us.

Coercion (as) is necessary when a cast known to be successful resolves an expression. In your case, winTriggers of type [MSWinGameTrigger] does not have a member named totalConditions, but it's supertype does, so Swift's type safety won't let you off without a coercion.


I must agree with Jon here: I suppose you understand that knocking out the door unless the history and background of the issue are clear to you is quite unreasonable.

3 Likes

I considered your notes and I'm pleased to inform everybody that this discussion proceeds from now in a slower roll ;)

The best you could do to avoid coercion is have separate extensions for : and == and delegate the homogeneous one to the heterogeneous

extension Collection where Element == MSGameTrigger {
    var totalConditions: [MSGameCondition] { ... }
}

extension Collection where Element: MSGameTrigger {
    var totalConditions: [MSGameCondition] { 
        return (self as [MSGameTrigger]).totalConditions
    }
}

It can't work, because protocols can define requirements that are static methods, which you can call on a type parameter itself. For example,

protocol P {
  static func foo()
}

extension Array where Element : P {
 func bar() {
    Element.foo()
  }
}

struct S1 : P {
  static func foo() {
    print("Hello from S1")
  }
}

struct S2 : P {
  static func foo() {
    print("Hello from S2")
  }
}

[S1()].bar() // prints "Hello from S1"
[S2()].bar() // prints "Hello from S2"

Now suppose you could do this:

let array: [P] = [S1(), S2()]
array.bar() // What would this print?
4 Likes

Nor really related to your question, but this code:

Could be simplified using a single .flatMap:

var totalConditions: [MSGameCondition] { 
    return flatMap { $0.conditions }
}
1 Like

Ok I see the problem. The solution is allow to constrain Element type without Element metatype.

Thanks everybody for other helpful answers.

It doesn't compile without as!. Error:

'Self' is not convertible to '[MSGameTrigger]'; did you mean to use 'as!' to force downcast?

Sorry, I didn’t notice you were extending Collection there. That indeed makes the approach incorrect; collections aren’t covariant in general. Are you sure you need to generalize that much though? Your code seems to be just fine with Array.

I'm always trying to make everything in a most general way. It's just a programmer's reflex :]

The point is that sometimes, unless you have an actual use case, it’s reasonable to keep things simple when the more general approach requires additional logic.

Anyway, If you need Collection, duplicating the logic for both extensions rather than delegating will work fine.