Search code examples
swiftgenerics

Conformance to generic constraint seems impossible with subclass


I've cut my code down to the following simplest version to show the problem. I'm not sure why Swift is complaining.

Why does DatabaseGateway need to conform to protocol Something ???

struct Database {}

open class DatabaseGateway {
    
    let database: Database
    
    public init() {
        database = Database()
    }
}

struct FilterParameter<T> {}

protocol Something {
    associatedtype T: RawRepresentable where T.RawValue == String
    var filterParameter: FilterParameter<T> { get }
}

enum VoteTable {}

extension VoteTable {
    
    enum Field: String {
        case value = "value"
    }
}

final class User: DatabaseGateway, Something {
    let filterParameter = FilterParameter<VoteTable.Field>()
}

final class Poll: DatabaseGateway, Something {
    let filterParameter = FilterParameter<VoteTable.Field>()
}

struct VoteRecordFilter {
    
    let parameters: [Parameter]
    
    init(parameter: Parameter) {
        self.init(parameters: [parameter])
    }
    
    init(parameters: [Parameter]) {
        self.parameters = parameters
    }
    
    init<S: Something>(parameters: [S]) where S.T == VoteTable.Field {
        self.init(parameters: parameters.map({
            $0.filterParameter
        }))
    }
        
    typealias Parameter = FilterParameter<VoteTable.Field>
}

extension Poll {
    
    func voteSubmitted(user: User) async throws -> Bool {
        let filter = VoteRecordFilter(parameters: [self, user]) // Initializer 'init(parameters:)' requires that 'DatabaseGateway' conform to 'Something'
        return true
    }
}

Solution

  • The initialiser you want to call takes a type parameter S. What should S be, in the call VoteRecordFilter(parameters: [self, user])?

    [self, user] is neither a [Poll] nor a [User]. Swift infers the expression's type to be [DatabaseGateway], and tries to use DatabaseGateway for S. DatabaseGateway does not conform to Something, hence the error. In fact, there is no type S that allows [self, user] to be passed to parameters:.

    Instead of making it generic, you can take an array of existential types,

    init(parameters: [any Something<VoteTable.Field>]) {
        self.init(parameters: parameters.map(\.filterParameter))
    }
    

    You should also add T as the primary associated type of Something for the Something<VoteTable.Field> to work.

    protocol Something<T> { // add '<T>' here
        associatedtype T: RawRepresentable where T.RawValue == String
        var filterParameter: FilterParameter<T> { get }
    }