Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Heterogeneous generic container in Swift

I have a problem to put structs with a generic type in one array. I know that Swift converts the meta type of an Array into a concrete type and that this is the conflict. I tried to find a different solution but I think I need your help.

Here I define the structs and protocols:

protocol ItemProtocol {
    var id: String { get }
}

struct Section<T: ItemProtocol> {
    var items: [T]
    var renderer: Renderer<T>
}

struct Renderer<T> {
    var title: (T) -> String
}

Here two example structs that implement the ItemProtocol:

struct Book: ItemProtocol {
    var id: String
    var title: String
}

struct Car: ItemProtocol {
    var id: String
    var brand: String
}

This is how i setup the sections:

let book1 = Book(id: "1", title: "Foo")
let book2 = Book(id: "2", title: "Bar")
let books = [book1, book2]
let bookSection = Section<Book>(items: books, renderer: Renderer<Book> { (book) -> String in
    return "Book title: \(book.title)"
})
let car1 = Car(id: "1", brand: "Foo")
let car2 = Car(id: "2", brand: "Bar")
let cars = [car1, car2]
let carSection = Section<Car>(items: cars, renderer: Renderer<Car> { (car) -> String in
    return "Car brand: \(car.brand)"
})

Now i want to put the sections together. Here is what i tried. But each of these 3 lines give me an error:

let sections: [Section<ItemProtocol>] = [bookSection, carSection]
let sections2: [Section] = [bookSection, carSection]
let sections3: [Section<AnyObject: ItemProtocol>] = [bookSection, carSection]

sections.forEach({ section in
    section.items.forEach({ item in
        let renderedTitle = section.renderer.title(item)
        print("\(renderedTitle)")
    })
})

For the declaration of the sections array i get this error:

Using 'ItemProtocol' as a concrete type conforming to protocol 'ItemProtocol' is not supported

For the declaration of the sections2 array this error:

Cannot convert value of type 'Section' to expected element type 'Section'

And sections3 throws this:

Expected '>' to complete generic argument list

like image 431
Lukas Würzburger Avatar asked Mar 13 '18 09:03

Lukas Würzburger


2 Answers

The struct Section is generic so you cannot use it as a type. One solution could be to use a type erasure:

Create any ItemProtocol wrapper:

protocol ItemProtocol {
    var id: String { get }
}

struct AnyItem : ItemProtocol {

    private let item: ItemProtocol

    init(_ item: ItemProtocol) {
        self.item = item
    }

    // MARK: ItemProtocol
    var id: String { return item.id }
}

And a type erased Section, Any section:

protocol SectionProtocol {
    associatedtype T
    var items: [T] { get }
    var renderer: Renderer<T> { get }
}

struct Section<Item: ItemProtocol>: SectionProtocol {
    typealias T = Item
    var items: [Item]
    var renderer: Renderer<Item>

    var asAny: AnySection {
        return AnySection(self)
    }
}

struct AnySection : SectionProtocol {
    typealias T = AnyItem

    private let _items: () -> [T]
    private let _renderer: () -> Renderer<T>

    var items: [T] { return _items() }
    var renderer: Renderer<T> { return _renderer() }

    init<Section : SectionProtocol>(_ section: Section) {
        self._items = { section.items as! [AnySection.T] }
        self._renderer = { section.renderer as! Renderer<AnySection.T>}
    }
} 

Now you can have a collection of AnySections:

let sections: [AnySection] = [bookSection.asAny, carSection.asAny]
like image 104
Glaphi Avatar answered Oct 04 '22 22:10

Glaphi


The problem is there is no common ground (excepting Any) between different Section types (with different generic arguments). One possible solution would be to unify all Section types into one protocol, and use that protocol to build the array:

protocol SectionProtocol {
    var genericItems: [ItemProtocol] { get }
    var renderedTitles: [String] { get }
}

extension Section: SectionProtocol {
    var genericItems: [ItemProtocol] { return items }
    var renderedTitles: [String] {
        return items.map { renderer.title($0) }
    }
}

let sections: [SectionProtocol] = [bookSection, carSection]

sections.forEach { section in
    section.renderedTitles.forEach { renderedTitle in
        print("\(renderedTitle)")
    }
}

So instead of iterating through the elements, you iterate through the rendered titles, which each section should be able to construct.

Now this addresses the basic use case from your question, however depending on you use the section in your app it might not be enough and you'll have to recourse to type erasers, as other answerers mentioned.

like image 26
Cristik Avatar answered Oct 04 '22 21:10

Cristik