[Re-Proposal] Type only Unions

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:

  1. 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

    1. if not then report an error
    2. 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
    
  2. when the union type calls a method, compiler checks if the method signature exists in all types

    1. if not then report an error
    2. if they all exist then check if the return type is the same
      1. if it is then return the same type
      2. 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
    
  3. when the union type value is used as parameter / error,compiler checks if all types can be used for this parameter,

    1. if not then report an error
    2. 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
    
  4. 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:

    1. considering protocol/class inheritance, switches should allow upward or downward casting.
    2. to reduce complexity, overlap can be supported
    3. 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:
              ...
      
1 Like