Let me explain the situation where my question was raised step by step.
(Side note: Whole code I'm going to show is there on gist)
Base Protocols
Here are two protocols: P and Q.
Q inherits from P and has an additional associatedtype.
protocol P {}
protocol Q: P {
associatedtype QA
var qa: QA { get }
}
Concrete Types that conform to P or Q
Let's implement concrete types:
ConcretePconforms toP.ConcreteQconforms toQ.
struct ConcreteP: P {}
struct ConcreteQ: Q {
typealias QA = Int
var qa: QA { 0 }
}
Container Protocols
Here are two other protocols:
PContainerhas itscontentwhich conforms toP.QContainerinherits fromPContainerand has itscontentwhich conforms toQ.
protocol PContainer<Content> where Content: P {
associatedtype Content
var content: Content { get }
}
protocol QContainer<Content>: PContainer where Content: Q {}
Concrete Type Container
- It basically conforms to
PContainer - It conditionally conforms to
QContainerwhenContentconforms toQ
struct Container<Content>: PContainer where Content: P {
var content: Content
init(_ content: Content) { self.content = content }
}
extension Container: QContainer where Content: Q {}
// Prepare two containers:
let pContainer = Container<ConcreteP>(ConcreteP())
let qContainer = Container<ConcreteQ>(ConcreteQ())
Test Case #1
We can detect whether or not Container conforms to QContainer at runtime.
extension Container {
func test1() {
if self is any QContainer<Content> {
print("I'm also `QContainer`.")
} else {
print("I'm just `PContainer`.")
}
}
}
pContainer.test1() // Prints "I'm just `PContainer`."
qContainer.test1() // Prints "I'm also `QContainer`."
Test Case #2
Of course, Content of any QContainer<Content> is always any Q.
extension Container {
func test2() {
if case let qSelf as any QContainer<Content> = self {
print("Content is `Q`: \(qSelf.content is any Q)")
}
}
}
qContainer.test2() // Prints "Content is `Q`: true"
Test Case #3
In theory, the compiler can statically know qSelf.content is any Q in the following code.
However, this code fails to be compiled (with -DCOMPILER_CAN_ASSERT_CONTENT_IS_Q option).
(That seems to be because Content constraints never change in if case ... { } scope.)
extension Container {
func test3() {
if case let qSelf as any QContainer<Content> = self {
#if COMPILER_CAN_ASSERT_CONTENT_IS_Q
print("Value of `qa` is: \(qSelf.content.qa)")
// ⛔️ error: value of type 'Content' has no member 'qa'
#endif
}
}
}
qContainer.test3() // ⛔️
Workaround #1
One of workarounds for Test Case #3 is to remove <Content> from as any QContainer.
- Pros: Very simple.
- Cons: The fact that
qSelf.content is Contentis lost in static analysis.
extension Container {
func workaround1() {
if case let qSelf as any QContainer = self {
print("Value of `qa` is: \(qSelf.content.qa)")
}
}
}
qContainer.workaround1() // Prints "Value of `qa` is: 0"
Workaround #2
Another workaround is explicit coercion for qSelf.content.
- Pros: Compiler can still assert
qSelf.content is Content. - Cons: Redundunt for humans.
extension Container {
func workaround2() {
if case let qSelf as any QContainer<Content> = self {
print("Value of `qa` is: \((qSelf.content as! any Q).qa)")
}
}
}
qContainer.workaround2() // Prints "Value of `qa` is: 0"
Workaround #3
Third workaround is to extend QContainer protocol.
- Pros: We can open the existential explicitly(?).
- Cons: It may be a hassle to write in some cases.
extension QContainer {
func printQA() {
print("Value of `qa` is: \(self.content.qa)")
}
}
extension Container {
func workaround3() {
if case let qSelf as any QContainer<Content> = self {
qSelf.printQA()
}
}
}
qContainer.workaround3() // Prints "Value of `qa` is: 0"
Question:
Do you think Test Case #3(func test3) should be compiled successfully in the future?