4

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?

Ashley Mills
  • 50,474
  • 16
  • 129
  • 160
  • Maybe there is some usefull information here: https://stackoverflow.com/questions/50075977/using-delegates-on-generic-protocol – J. Doe May 18 '18 at 10:00
  • `QueryParameters` is a protocol, not a type. You don't seem to have defined it as a `struct` or `class` etc. Thus `func parameters() -> QueryParameters` does not make sense. I am not an expert on these things however, so I'm open to correction. – Chris May 18 '18 at 10:02
  • @Chris - I want the func to return an object that conforms to the `QueryParameters` protocol. I've spent so long looking at this I've gone code-blind. Can't tell what's right or wrong any more! – Ashley Mills May 18 '18 at 10:03
  • You could declare `Parameter` as a `struct` and declare its conformance to `QueryParameters` in its declaration: `struct Parameter: QueryParameters { }` and then function could be `func parameters() -> Parameter` – Chris May 18 '18 at 10:36
  • 1
    Code-blind is my basic state! :) – Chris May 18 '18 at 10:39
  • The bottom line is you have an `associatedtype` and nowhere in your code do you tell what type it should be. – Au Ris May 18 '18 at 10:45
  • @AuRis Any idea how I resolve that? – Ashley Mills May 18 '18 at 10:46
  • Have a look at [Protocol can only be used as a generic constraint because it has Self of associatedType requirements](https://stackoverflow.com/questions/36348061/protocol-can-only-be-used-as-a-generic-constraint-because-it-has-self-or-associa) – Dávid Pásztor May 18 '18 at 10:56
  • There are a few problems: `QueryParameters` declares `get` variables, but your struct doesn't conform to that requirement, also it is unknown how you can initialize the `Filters` object as it never becomes a type. If I have time I'll write an answer later. – Au Ris May 18 '18 at 10:59
  • @AuRis `TransactionParameters` does conform to `QueryParameters` - it has `page` and `filters`. Am I missing something? And `filters` is set to `TransactionFilters` - a concrete type – Ashley Mills May 18 '18 at 11:01
  • `TransactionParameters` does not conform to `QueryParameters` because in `QueryParameters` both properties are declared as `get` and in `TransactionParameters` they are `set` and `get`. – Au Ris May 18 '18 at 11:50
  • @AshleyMills please check my answer and let me know if it helps. – Au Ris May 20 '18 at 10:33

2 Answers2

6

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)
Au Ris
  • 4,541
  • 2
  • 26
  • 53
1

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 :)

Lucas Chwe
  • 2,578
  • 27
  • 17