Search code examples
swiftgenericsswift-protocolsassociated-typesgeneric-function

Conform class extension to generic protocol function


* 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?

Solution

  • 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())