Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Protocol can an only be used as a generic constraint because it has Self or associated type requirements

Following on from an answer to my previous question, I have 2 protocols…

protocol Filters: Encodable { 
}

protocol QueryParameters: Encodable {
    associatedtype T: Filters
    var page: Int { get }
    var filters: T { get }
}

and then for a type Transaction, I have…

struct TransactionFilters: Filters {
    var isWithdrawal: Bool
}

struct TransactionParamters<T: Filters>: QueryParameters {
    var page: Int
    var filters: T
}

All good so far.

Next I add a protocol, Filterable, and I'd like any type that conforms to Filterable to be able to return parameters, like this…

protocol Filterable {
    func parameters() -> QueryParameters
}

struct Transactions: Filterable {
    func parameters() -> QueryParameters {
        let transactionFilters = TransactionFilters(isWithdrawal: true)
        return TransactionParamters(page: 1, filters: transactionFilters)
    }
}

but I end up with…

error: protocol 'QueryParameters' can only be used as a generic constraint because it has Self or associated type requirements

This seems a pretty straightforward requirement, but I've spent 2 days trying all combinations I could think of to get it to work. Now I'm finally admitting defeat.

What do I need to do to resolve this?

like image 494
Ashley Mills Avatar asked Oct 20 '25 14:10

Ashley Mills


2 Answers

As I mentioned in the comment. What is missing in your code is that the associatedtype never actually becomes a type. Nowhere in the code in one of your structs you assign the type to the associatedtype. If you want a generic filterable functionality you could do something along those lines:

// Your generic Filters with required properties
protocol Filters: Encodable {
    var isWithdrawal: Bool { get }
    init(isWithdrawal: Bool)
}

// Your generic query parameters
protocol QueryParameters: Encodable {
    associatedtype F: Filters
    var page: Int { get }
    var filters: F { get }

    init(page: Int, filters: Filters)
}

// Filterable protocol will eventually accept any types conforming to Filters and QueryParameters
protocol Filterable {
    associatedtype F: Filters
    associatedtype P: QueryParameters

    func parameters() -> P
}

// This is your generic Transactions struct
// With this you will be able to pass other types that meet the constraints
struct Transactions<ParameterType: QueryParameters>: Filterable {
    typealias P = ParameterType
    typealias F = ParameterType.F

    func parameters() -> ParameterType {
        return P(page: 1, filters: F(isWithdrawal: true))
    }
} 

You're done with the generic stuff. Now you can create different model objects which conform to your protocols

struct TransactionFilters: Filters {
    private(set) var isWithdrawal: Bool // Conforming to filters
}

struct TransactionParameters<FilterType: Filters>: QueryParameters {
    // Telling what type is the object that conforms to Filters
    typealias F = FilterType
    var page: Int
    var filters: FilterType

    init(page: Int, filters: Filters) {
        self.page = page
        self.filters = filters as! F
    }
}

Create your transactions object like this:

let transactions = Transactions<TransactionParameters<TransactionFilters>>()
print(transactions.parameters().page)
print(transactions.parameters().filters.isWithdrawal)

You can create more types of QueryParameters and Filters

struct SomeOtherParameters<FilterType: Filters>: QueryParameters {
    // You can do custom stuff in your SomeOtherParameters struct
    // e.g. add an offset to page
    typealias F = FilterType
    var page: Int
    var filters: FilterType

    init(page: Int, filters: Filters) {
        self.page = page + 100
        self.filters = filters as! F
    }
}

// Your special filter that always returns false
struct SomeOtherFilters: Filters {
    init(isWithdrawal: Bool) {}

    var isWithdrawal: Bool {
        return false
    }
}

let transactionsWithDifferentFilters = Transactions<SomeOtherParameters<SomeOtherFilters>>()

// You can combine any types that conform to you declared protocols
let evenMoreTransactions = Transactions<SomeOtherParameters<TransactionFilters>>()
print(evenMoreTransactions.parameters().page)
like image 157
Au Ris Avatar answered Oct 22 '25 04:10

Au Ris


A little late, but I think it's still a good exercise to come up with a clean solution for your problem.

Within the Filterable protocol, you can't use the protocol type QueryParameters as a return type because QueryParameters has an associatedtype. Also, it's not really what you want, because returning a protocol type doesn't mean it conforms to itself (ie. QueryParameters type doesn't conform to QueryParameters).

Instead, what you need to do is create an associatedtype that conforms to the protocol QueryParameters to be used as a return type:

protocol Filterable {
    associatedtype T : QueryParameters
    func parameters() -> T
}

Then you can make Transactions conform to Filterable using the same function you have in your question but returning the opaque type some QueryParameters rather than the protocol type QueryParameters. This way you can return any type that conforms to QueryParameters which is what you really wanted to do:

struct Transactions: Filterable {
    func parameters() -> some QueryParameters {
        let transactionFilters = TransactionFilters(isWithdrawal: true)
        return TransactionParameters(page: 1, filters: transactionFilters)
    }
}

I hope you find that helpful. BTW, I fixed the typo in TransactionParameters in my answer :)

like image 45
Lucas Chwe Avatar answered Oct 22 '25 04:10

Lucas Chwe