I'm not sure exactly how to describe this unexpected behavior with protocols and generics, so I'll give a distilled example:
My app works with a certain kind of element (conforming to a protocol that I'll refer to as Element
) and containers/groupings of them (conforming to a Container
protocol). Containers are elements themselves, and can be nested (ex: container within a container). My use case is unique, but the structure is common (like a basic Array or a SwiftUI stack view).
protocol Element {}
protocol Container: Element {
associatedtype Child: Element
var elements: [Child] { get }
}
// Some types that conform to those protocols
struct ExampleElement: Element {}
struct ExampleContainer<Child: Element>: Container {
var elements: [Child]
}
I'll define a method for all containers, to do something with their elements. At the moment, these examples simply print whether or not the container's child elements are also containers themselves.
extension Container {
func action() {
print("Child is a simple Element.")
}
func action() where Child: Container {
print("Child is a Container")
elements.forEach { $0.action() }
}
}
This works fine. And I can test it like so:
let nestedContainer = ExampleContainer(elements: [ ExampleElement() ])
let doubleNestedContainer = ExampleContainer(elements: [nestedContainer])
doubleNestedContainer.action()
// Prints:
// Child is a Container
// Child is a simple Element.
This is what I would expect. The doubleNestedContainer
is recognized correctly as a container of containers, so the appropriate action()
method is called. But it doesn't work if I define a triple nested container.
let tripleNestedContainer = ExampleContainer(elements: [doubleNestedContainer])
tripleNestedContainer.action()
// Prints:
// Child is a Container
// Child is a simple Element.
I would have expected this:
// Child is a Container
// Child is a Container
// Child is a simple Element.
It seems that the version of the action()
method where Child: Container
only runs for the top-most parent container. Swift doesn't seem to recognize that the child containers conform to the Container
protocol. The only way to get my expected result is to define yet another method with a second where
clause:
extension Container {
func action() where Child: Container, Child.Child: Container {
print("Child is a Container")
elements.forEach { $0.action() }
}
}
tripleNestedContainer.action()
// Prints:
// Child is a Container
// Child is a Container
// Child is a simple Element.
I expected that Swift would be able to recognize a type's conformance in this scenario. Similar to how an Array is Equatable if its element is Equatable, and that applies recursively to Arrays of Arrays. That demonstrates enough type "awareness" that I assumed my situation would work too. In fact it took me half a day to identify that this was the cause of a bug.
I'm wondering 1. Why is this the case? and 2. Is there some way to accomplish the kind of action()
method I expect, that drills down through containers until it gets to the ultimate child element?