I've briefly drafted the definition of Common part, and if there are no problems in the next few days I'll update it in the post:
*Updated based on Nickolas's idea
What the compiler needs to do is:
-
when the parameter/variable type is union type, the compiler checks that the type of the argument is a true subset of the union type
- if not then report an error
- if yes then compile success
protocol P {} protocol Q {} struct S: P, Q {} struct T: Q {} func test(_ value: any P | any Q) { } test(S()) // ok test(T()) // ok
protocol O {} protocol P: O {} protocol Q: P {} struct S: O {} struct T: P {} struct U: Q {} func test(_ value: any O | any P) { } test(S()) // ok test(T()) // ok test(U()) // ok
class A { } class B<T> { } class C: B<Int> { } class D: B<String> { } var value: A | B<Int> = A() // ok view = B<Int>() // ok view = C() // ok view = D() // fail
protocol P1 { associatedtype T } protocol P2: P1 where T == String { } class A: P1 { typealias T = Int } class B: P1 { typealias T = Int } class C: P2 { } class D: P1 { typealias T = String } var value: A | P2 = A() // ok view = B() // fail view = C() // ok view = D() // fail
-
when the union type calls a method, compiler checks if the method signature exists in all types
- if not then report an error
- if they all exist then check if the return type is the same
- if it is then return the same type
- if it is not then return a new union type
struct S { var foo: () -> Int var bar: String = “” } struct R { func foo(_ x: String = “”) -> Int { 42 } func foo() -> Double { 3.0 } var bar: [Int] = [] } let x: S | R = R() let y = x.foo() // y has type Int let z = x.bar // z has type (String | [Int])
protocol P { associatedtype Assoc func getAssoc() -> Assoc } struct A<T> { public let wrapped: T func getAssoc() -> T { wrapped } } struct B { func getAssoc() -> Int { 42 } } func f<T>(x: A<T> | B) { let z = x.getAssoc() // z has type (T | Int) }
let view: NSTableView | NSCollectionView view.isOpaque = false // setter (isOpaque: Bool) can be found in both NSTableView and NSCollectionView
-
when the union type value is used as parameter / error,compiler checks if all types can be used for this parameter,
- if not then report an error
- if yes then compile success
enum ErrorFoo: Error { } enum ErrorBar: Error { } let error: ErrorFoo | ErrorBar throw error // throw need a Swift.Error, and ErrorFoo and ErrorBar both meet the requirements
protocol P1 { associatedtype T } protocol P2: P1 where T == String { } class A: P1 { typealias T = Int } class B: P1 { typealias T = Int } class C: P2 { } class D: P1 { typealias T = String } func test1(_ value: any P1) { } let value1: A | B test1(value1) // ok let value2: A | C test1(value1) // ok func test2<V: P1>(_ value: V) where V.T == String { } test2(value1) // fail test2(value2) // fail let value3: C | D test2(value3) // ok
-
when switching a union, its behavior should be similar to switch an Any value:
let value: A | B switch value { case let value as A: ... case let value as B: ... }
However, to simplify implementation, compiler can compare the type of each case with the union types, and if there is consistency, the type is assumed to be covered. To accomplish this, the compiler needs to support the following:
- considering protocol/class inheritance, switches should allow upward or downward casting.
- to reduce complexity, overlap can be supported
- check if all union types have been switched, it is recommended to check type and its downward only if all types have been included, if not, it should report the error.
protocol A { } protocol B { } protocol C: A { } let value: A | B // ✅ exhaustive switch value { case let value as A: ... case let value as B: ... } // ✅ downward casting switch value { case let value as B: ... case let value as C: ... } // ✅ downward casting switch value { case let value as B: ... case let value as Any: ... }
let value: B | C // ✅ exhaustive switch value { case let value as C: ... case let value as B: ... } // ✅ overlap switch value { case let value as A: ... case let value as B: ... case let value as C: ... // This case will never be triggered, it would be nice to give a warning. } // ❌ error: Switch must be exhaustive switch value { case let value as A: ... case let value as B: ... } // ✅ exhaustive switch value { case let value as A: ... case let value as B: ... default: ...