Type erasing where protocol has Self requirements

I am struggling with building a type eraser for a Collection wrapper. The protocols are simplified and are defined by a package, so I can't change them. Also I can't use any new iOS13 features like opaque result types.

I have created quite a few type erasers before and never really had any issues, but this time I am stuck. The problem is the Self requirement in the base protocol.

Here is the code I have so far, which does everything except I can't see how I can forward the calls to isEqual to my box, as the base class is not adopting the erased protocol, but rather the subclass. (Precedent in the standard lib is _AnyIteratorBox). If the abstract base class would adopt the protocol, I'd have a problem implementing the initializer requirement in the subclass :frowning:.

Do I need another type-eraser? I have a gut feeling somehow…
Any help would be greatly appreciated.

import Foundation

// These protocols are a given and not up for change

protocol Diffable {
    func isEqual(to: Self) -> Bool
}

protocol Container: Diffable {
    associatedtype Collection: Swift.Collection where Collection.Element: Diffable
    init<C: Swift.Collection>(source: Self, elements: C) where C.Element == Collection.Element
    var elements: Collection { get }
}

// A type eraser for containers of the same element type

class AnyContainer<Element: Diffable>: Container {
    var _box: _AnyContainerBoxBase<Element>
    
    init<F>(_ container: F) where F: Container, F.Collection.Element == Element {
        _box = _AnyContainerBox(container)
    }
    
    required init<C>(source: AnyContainer<Element>, elements: C) where C: Swift.Collection, C.Element == Element {
        self._box = _AnyContainerBox(source: _AnyContainerBox(source), elements: elements)
    }
    
    var elements: [Element] { _box.elements }
    func isEqual(to: AnyContainer<Element>) -> Bool {
        fatalError("What now…?") // <- how is this possible to implement?
    }
}

// Conforming this to `Foo` instead of the base class because if the initializer requirement
class _AnyContainerBox<F: Container>: _AnyContainerBoxBase<F.Collection.Element>, Container {
    var container: F
    
    init(_ container: F) {
        self.container = container
    }
    
    required init<C>(source: _AnyContainerBox<F>, elements: C) where C: Swift.Collection, C.Element == F.Collection.Element {
        self.container = F.init(source: source.container, elements: elements)
    }
    
    override var elements: [F.Collection.Element] {
        Array(container.elements)
    }
    
    // This doesn't override, so it doesn't work as intended :(
    func isEqual(to: _AnyContainerBox<F>) -> Bool {
        container.isEqual(to: to.container)
    }
}

class _AnyContainerBoxBase<Element> {
    var elements: [Element] { fatalError("Abstract") }
    // Sadly, this is what would get called always
    func isEqual(to: _AnyContainerBoxBase<Element>) -> Bool { fatalError("Abstract") }
}

Assuming the wrapped containers must be of the same type:

// AnyContainer
func isEqual(to other: AnyContainer<Element>) -> Bool {
    return _box.isEqual(to: other._box)
}

// _AnyContainerBoxBase
func isEqual(to: _AnyContainerBoxBase<Element>) -> Bool { fatalError("Abstract") }

// _AnyContainerBox
override func isEqual(to other: _AnyContainerBoxBase<F.Collection.Element>) -> Bool {
    return (other as? Self)?.container.isEqual(to: container) ?? false
}

I don't see any reason for _AnyContainerBox to conform to Container.

No, the wrapped containers should not be of the same type (hah, that would be too easy ;) ). Just their elements.
If they are not of the same type they are of course not equal.

As a bit of context: The containers are sections in a table, where each section will have its own observer's or custom data, but the elements of all sections will be cell descriptors (which are type-erased wrappers around different cell classes basically)

Then you don't need to implement isEqual(to:) in your box at all. Just do it in AnyContainer:

// AnyContainer
func isEqual(to other: AnyContainer<Element>) -> Bool {
    let mine = elements
    let others = other.elements
    return mine.count == others.count && zip(mine, others).allSatisfy { $0.isEqual(to: $1) }
}

Which seems so obvious, and I feel so stupid now…

I'll test that out tomorrow but imagine me shaking my head in shame in bed right now… :joy:

Thank you Rob!

Ahh wait!! NO!

isEqual of the container is NOT equality of its elements! They are both Diffable on their own!

The container (think section) has a title for example, and if the title changes, the section header needs to change. the elements stay the same and will not result in any changes in the table.

That is the actual protocols this is all about btw: