Last active
February 27, 2026 03:57
-
-
Save YOCKOW/1e14076994e4090890d01e7fc0337cf3 to your computer and use it in GitHub Desktop.
How deeply will the compiler infer the types behind parameterized protocols?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Thread: https://forums.swift.org/t/how-deeply-will-the-compiler-infer-the-types-behind-parameterized-protocols/85038 | |
| /* | |
| **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: | |
| - `ConcreteP` conforms to `P`. | |
| - `ConcreteQ` conforms to `Q`. | |
| */ | |
| struct ConcreteP: P {} | |
| struct ConcreteQ: Q { | |
| typealias QA = Int | |
| var qa: QA { 0 } | |
| } | |
| /* | |
| **Container Protocols** | |
| Here are two other protocols: | |
| - `PContainer` has its `content` which conforms to `P`. | |
| - `QContainer` inherits from `PContainer` and has its `content` which conforms to `Q`. | |
| */ | |
| 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 `QContainer` when `Content` conforms to `Q` | |
| */ | |
| 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 Content` is 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? | |
| */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment