Search code examples
iosswiftgenericsswift2swift-protocols

Protocol can only be used as a generic constraint because it has Self or associatedType requirements


I have a protocol RequestType and it has associatedType Model as below.

public protocol RequestType: class {

    associatedtype Model
    var path: String { get set }

}

public extension RequestType {

    public func executeRequest(completionHandler: Result<Model, NSError> -> Void) {
        request.response(rootKeyPath: rootKeyPath) { [weak self] (response: Response<Model, NSError>) -> Void in
            completionHandler(response.result)
            guard let weakSelf = self else { return }
            if weakSelf.logging { debugPrint(response) }
        }
    }

}

Now I am trying to make a queue of all failed requests.

public class RequestEventuallyQueue {

    static let requestEventuallyQueue = RequestEventuallyQueue()
    let queue = [RequestType]()

}

But I get the error on line let queue = [RequestType]() that Protocol RequestType can only be used as a generic constraint because it has Self or associatedType requirements.


Solution

  • Suppose for the moment we adjust your protocol to add a routine that uses the associated type:

    public protocol RequestType: class {
        associatedtype Model
        var path: String { get set }
        
        func frobulateModel(aModel: Model)
    }
    

    And Swift were to let you create an array of RequestType the way you want to. I could pass an array of those request types into a function:

    func handleQueueOfRequests(queue: [RequestType]) {
        // frobulate All The Things!
    
        for request in queue {
           request.frobulateModel(/* What do I put here? */)
        }
    }
    

    I get down to the point that I want to frobulate all the things, but I need to know what type of argument to pass into the call. Some of my RequestType entities could take a LegoModel, some could take a PlasticModel, and others could take a PeanutButterAndPeepsModel. Swift is not happy with the ambiguity so it will not let you declare a variable of a protocol that has an associated type.

    At the same time it makes perfect sense to, for example, create an array of RequestType when we KNOW that all of them use the LegoModel. This seems reasonable, and it is, but you need some way to express that.

    One way to do that is to create a class (or struct, or enum) that associates a real type with the abstract Model type name:

    class LegoRequestType: RequestType {
      typealias Model = LegoModel
    
      // Implement protocol requirements here
    }
    

    Now it's entirely reasonable to declare an array of LegoRequestType because if we wanted to frobulate all of them we know we would have to pass in a LegoModel each time.

    This nuance with Associated Types makes any protocol that uses them special. The Swift Standard Library has Protocols like this most notably Collection or Sequence.

    To allow you to create an array of things that implement the Collection protocol or a set of things that implement the sequence protocol, the Standard Library employs a technique called "type-erasure" to create the struct types AnyCollection<T> or AnySequence<T>. The type-erasure technique is rather complex to explain in a Stack Overflow answer, but if you search the web there are lots of articles about it.

    Swift 5.7 Existentials

    Swift 5.7 introduces explicit existential using the any keyword. This will remove the "Protocol can only be used as a generic constraint…" error, but it doesn't solve the fundamental problem with this example. (Admittedly this example is academic, for demonstration purposes, and likely not useful in real code because of its limitations. But it also demonstrates how explicit existentials aren't a panacea.)

    Here is the code sample using Swift 5.7 and the any keyword.

    public protocol RequestType: AnyObject {
        associatedtype Model
        var path: String { get set }
    
        func frobulateModel(aModel: Model)
    }
    
    func handleQueueOfRequests(queue: [any RequestType]) {
        // frobulate All The Things!
    
        for request in queue {
           request.frobulateModel(/* What do I put here? */)
        }
    }
    

    Now our queue contains a collection of existentials and we no longer have the error about the "type cannot be used here because of Self or AssociatedType constraints". But it doesn't fix the underlying problem in this example because the frobulateModel method can still take an arbitrary type (the associated type of the entity conforming to the RequestType protocol).

    Swift provides other mechanisms that can help compensate for this. In general you'd want constrain the Model value to expose behavior shared by all Models. The frobulateModel method might be made generic and have constraints on the parameter to follow that protocol. Or you could use Swift 5.7's primary associated types (SE-0346) to help constrain the behavior of Models at the protocol level.

    So yes, explicit existentials can remove the error message that the OP asked about - but they are not a solution for every situation.

    Also, keep in mind that existentials lead to indirection and that can introduce performance problems. In their WWDC session, Apple cautioned us to use them judiciously.