1
votes

* Short version *

How can I conform a class (extension) to a generic protocol function?

* Long version *

This is a small part of a data structure to support a paginated collection,

protocol Pageable { 
     //an object whose can be in a collection
}

protocol Page{ //a page of a collection that can be paginated

    associatedtype PageItemType

    func itemAt<PageItemType:Pageable>(index: Int) -> PageItemType  
}

//Bonus question
//class PagedCollection<PageType:Page, ItemType:Pageable> {
    //...
//}

Here is the implementation of the protocols with a "real" case:

class Person : Pageable{}

class People {
    var people: [Person]?
}

//Mark: - Page

extension People: Page{ /*** error 1 ***/

    typealias PageItemType = Person

    func itemAt(index: Int) -> Person{
        let person : Person = self.people![index]
        return person
    }
}

Obtaining the following error (1):

Type 'People' does not conform to protocol 'Page'

Protocol requires nested type 'PageItemType'

I also tried making it explicit but i just got a different error:

//Mark: - Page

extension People: Page{

    typealias PageItemType = Person

    func itemAt<PageItemType:Pageable>(index: Int) -> PageItemType{
        let person : Person = self.people![index]
        return person /*** error 2 ***/
    }
}

Obtaining the following error (2):

Cannot convert return expression of type 'Person' to return type 'PageItemType'

So: *How can i let itemAt function return a valid type for the PageItemType typealias?

* Bonus *

Bonus question worth a 50 bounty (if answer is longer than a row i'll open a new question): Referring to the first code snippet PagedCollection

  • given that each Page implementation has always a known implementation of Pageable protocol objet type
  • is there a way to avoid declaring ItemType:Pageable ? Or at least enforce it with a where clause?
2
I guess the problem has to do with Swift not being able to infer that Person is a Pageable valud for PageItemTypejalone

2 Answers

4
votes

It looks like you're conflating associated types with generic functions.

Generic functions allow you to provide a type to replace a given generic placeholder at the call site (i.e when you call the function).

Associated types allow types conforming to protocols to provide their own type to replace a given placeholder type in the protocol requirements. This is done per type, not at the call site of any function. If you wish to enforce a conformance requirement for an associatedtype, you should do so directly on its declaration (i.e associatedtype PageItemType : Pageable).

If I understand your requirements correctly, your itemAt(index:) function should be non-generic (otherwise the associatedtype in your protocol is completely redundant). The type that it returns is defined by the implementation of the type that conforms to Page, rather than the caller of the function. For example, your People class defines that the PageItemType associated type should be a Person – and that is what itemAt(index:) should return.

protocol Pageable {/* ... */}

protocol Page {

    // any conforming type to Page will need to define a
    // concrete type for PageItemType, that conforms to Pageable
    associatedtype PageItemType : Pageable

    // returns the type that the implementation of the protocol defines
    // to be PageItemType (it is merely a placeholder in the protocol decleration)
    func itemAt(index: Int) -> PageItemType
}

class Person : Pageable {/* ... */}

class People {
    var people: [Person]?
}

extension People : Page {

    // explicitly satisfies the Page associatedtype requirement.
    // this can be done implicitly be the itemAt(index:) method,
    // so could be deleted (and annotate the return type of itemAt(index:) as Person)
    typealias PageItemType = Person

    // the itemAt(index:) method on People now returns a Person
    func itemAt(index: Int) -> PageItemType {

        // I would advise against force unwrapping here.
        // Your people array most likely should be non-optional,
        // with an initial value of an empty array
        // (do you really need to distinguish between an empty array and no array?)
        let person = self.people![index]
        return person
    }
}

In regards to your implementation of a PagedCollection – as the PageItemType associated type in your Page protocol conforms to Pageable, I see no need for an ItemType generic parameter. This can simply be accessed through the associated type of the given PageType generic parameter.

As an example:

class PagedCollection<PageType:Page> {

    // PageType's associatedtype, which will conform to Pageable.
    // In the case of the PageType generic parameter being People,
    // PageType.PageItemType would be of type Person
    var foo : PageType.PageItemType

    init(foo: PageType.PageItemType) {
        self.foo = foo
    }
}

// p.foo is of type Person, and is guarenteed to conform to Pageable
let p = PagedCollection<People>(foo: Person())
1
votes

This is merely a formatted discussion from my chat with Hamish.

I'm writing it here, because chatrooms can get archived and sometimes a more direct QA can convey things differently

Me:

protocol Provider { 
    associatedtype Input 
    associatedtype Output 
    func value(forKey _key: Input) -> Output{} 
}

vs.

protocol Provider{ 
    func value<Input, Output>(forKey _key: Input) -> Output{} 
}

Hamish: Essentially the difference is where the placeholders are satisfied – associated types are satisfied at type level, whereas generic placeholders on a function are satisfied at the call-site of said function.

Me: I understand that level of difference. But is there any byproduct due to that difference :D

Hamish: What do you mean? they express different things – the former is a protocol where the conforming type only deals with one specific type of input and output, the latter is a protocol where the conforming type can deal with any given input and output types.

Me: The former is a protocol where the conforming type only deals with one specific type of input and output. What do you mean by specific? Can't the typeAlias be anything I like?

Hamish: It can, but it can only be satisfied once per type – once I define

struct S : Provider { 
   func value(forKey: String) -> Int 
}

S now can only deal with String inputs and Int outputs. Whereas with the latter, I would define

struct S : Provider { 
    func value<Input, Value>(forKey _key: Input) -> Output{} 
} 

now S has to be able to deal with any input and output type.