swiftgenericsprotocolsswift-protocolsswift5.7

Protocol with generics throws error when used as a property to call a method


I have a protocol SomeObjectFactory whose method createSomeObjectWithConfiguration(_ config: SomeObjectConfiguration<T>) is used inside the class Builder. When I tried to compile this code with Swift 5.7 I ran into an error

Member 'configWithExperience' cannot be used on value of type 'any configurationFactory'; consider using a generic constraint instead

Here is the implementation below

import Combine
import Foundation

final class SomeObject<T: Combine.Scheduler> {}

struct Experience {
    let id: String
}

struct SomeObjectConfiguration<T: Combine.Scheduler> {
    let scheduler: T
}

protocol SomeObjectFactory {
    associatedtype T: Combine.Scheduler
    func createSomeObjectWithConfiguration(_ config: SomeObjectConfiguration<T>) -> SomeObject<T>
}

protocol ConfigurationFactory {
    associatedtype T: Combine.Scheduler
    func configWithExperience(_ experience: Experience) -> SomeObjectConfiguration<T>
}

final class Builder<T: Combine.Scheduler> {
    
    private let configurationFactory: any ConfigurationFactory
    
    init(configurationFactory: any ConfigurationFactory) {
        self.configurationFactory = configurationFactory
    }
    
    func createSomeObject(_ experience: Experience) {
        let someObjectConfiguration: SomeObjectConfiguration<T> = configurationFactory.configWithExperience(experience)
    }
}

I was hoping to create a someObjectConfiguration from the configurationFactory instance of the Builder.


Solution

  • any ConfigurationFactory really does mean, any ConfigurationFactory. There's no guarantee the ConfigurationFactory.T of that factory will be the same as the Builder.T that contains it. You need to add a constraint that those two should match.

    One way to do that is with a primary associated type, like so:

    // Make `T` a primary associated type
    // https://github.com/apple/swift-evolution/blob/main/proposals/0346-light-weight-same-type-syntax.md
    
    +protocol ConfigurationFactory<T> {
    -protocol ConfigurationFactory {
        associatedtype T: Combine.Scheduler
        func configWithExperience(_ experience: Experience) -> SomeObjectConfiguration<T>
    }
    
    final class Builder<T: Combine.Scheduler> {
    
    +    // Constant the `T` of the factory to be the `T` of this builder   
    +    private let configurationFactory: any ConfigurationFactory<T>
    -    private let configurationFactory: any ConfigurationFactory
        
         // Likewise, update the initializer to match
    +    init(configurationFactory: any ConfigurationFactory<T>) {
    -    init(configurationFactory: any ConfigurationFactory) {
            self.configurationFactory = configurationFactory
        }
        
        func createSomeObject(_ experience: Experience) {
            let someObjectConfiguration: SomeObjectConfiguration<T> = configurationFactory.configWithExperience(experience)
        }
    }