[Re-Proposal] Type only Unions

I rewrote Motivation based on Jumhyn's view.

Here's a draft, which I'll still update in the main post in a few days.

Motivation

If a developer now wants to match multiple types in Swift, here's how to do:

  1. enum

    enum CodablePrimitiveValue: Codable {
        case string(String)
        case int(Int64)
        ...
    }
    let value: CodablePrimitiveValue
    
    switch value {
    case .string(let string):
        print(string)
    case .int(let value):
        print(value)
    ...
    }
    

    Advantages of enum:

    • The available types are fixed, and can be iterated over with (switch).
    • All available types can be quickly accessed via the (. ) syntax.

    Disadvantages:

    • Need to unpack one extra time to use internal value's methods/properties
      let value: CodablePrimitiveValue
      switch value {
      case .string(let value): encode(value)
      case .int(let value): encode(value)
      ...
      }
      
  2. protocol

    protocol CodablePrimitiveValueType: Codable {
    }
    
    extension String: CodablePrimitiveValueType {
    }
    
    extension Int: CodablePrimitiveValueType {
    }
    ...
    
    let value: CodablePrimitiveValue
    
    if let value = value as? String {
        // ...
    } else if let value = value as Int {
        // ...
    }
    ...
    

    Advantages of protocol:

    • Developers are free to expand the supported types without worrying about the stability of the API.

    Disadvantages:

    • If the API provider doesn't implement the shortcut static property manually, the caller needs to manually find the type that implements the protocol, and there are plenty of APIs for this in SwiftUI:
      extension PrimitiveButtonStyle where Self == BorderlessButtonStyle {
          public static var borderless: BorderlessButtonStyle { ... }
      }
      
    • API providers can't restrict specific types.
    • Due to the uncertainty of the type, the compiler can't optimize it either
      • e.g., if the API provider provides multiple types, but the caller only uses one of them, the compiler should be able to optimize for the API if it's inlinable.

Enum and protocol also have these disadvantages:

  • New datatypes need to be defined for constraints, which increases the binary size, I'll explain later why this problem is solved based on compile-time unions
  • When all internal values have some kind of commonality (protocol or super class), there is no way to use it directly.
    • enum needs to implement an additional method to return a value with this commonality
      extension CodablePrimitiveValue {
          var value: Codable {
              switch self {
              case .string(let value): return value
              case .int(let value): return value
              case .uint(let value): return value
              case .bool(let value): return value
              case .double(let value): return value
              case .null: return String?.none
              }
          }
      }
      
    • protocol needs to be inherited or type-restricted by where, but restricting by where only restricts one type, and cannot be extended further.
      protocol CodablePrimitiveValue: Codable {
      }
      
      class PrimitiveValue {
      }
      
      protocol CodablePrimitiveValue where Self: PrimitiveValue {
      }
      
  1. function overloading

    func encode(_ value: String) {
    
    }
    func encode(_ value: Int) {
        
    }
    

    Advantages of overloading:

    • Intuitive, compiler can match and optimize better

    Disadvantages.

    • When there are a lot of parameters, it's a pain to match multiple versions for just one parameter.
    func makeNetworkRequest(
        urlString: String, method: String, headers: [String: String], body: Data?, timeout: TimeInterval, cachePolicy: URLRequest.CachePolicy, allowsCellularAccess: Bool, httpShouldHandleCookies: Bool, httpShouldUsePipelining: Bool, networkServiceType: URLRequest.NetworkServiceType, completion: @escaping (Result<Data, Error>) -> Void
    ) {
        let url = URL(url)
        makeNetworkRequest(url: url, ...)
    }
    
    func makeNetworkRequest(
        url: URL, method: String, headers: [String: String], body: Data?, timeout: TimeInterval, cachePolicy: URLRequest.CachePolicy, allowsCellularAccess: Bool, httpShouldHandleCookies: Bool, httpShouldUsePipelining: Bool, networkServiceType: URLRequest.NetworkServiceType, completion: @escaping (Result<Data, Error>) -> Void
    ) {
        let urlRequest = URLRequest(url)
        makeNetworkRequest(urlRequest: urlRequest, ...)
    }
    
    func makeNetworkRequest(
        urlRequest: URLRequest, method: String, headers: [String: String], body: Data?, timeout: TimeInterval, cachePolicy: URLRequest.CachePolicy, allowsCellularAccess: Bool, httpShouldHandleCookies: Bool, httpShouldUsePipelining: Bool, networkServiceType: URLRequest.NetworkServiceType, completion: @escaping (Result<Data, Error>) -> Void
    ) { 
        ...
    }
    

    At this point the developer is forced to use a protocol or an enum, which brings us back to the previous problem.

    protocol Requestable {
        var urlRequest: URLRequest { get }
    }
    extension String: Requestable {
        var urlRequest: URLRequest { ... }
    }
    extension URL: Requestable {
        var urlRequest: URLRequest { ... }
    }
    extension URLRequest: Requestable {
        var urlRequest: URLRequest { self }
    }
    func makeNetworkRequest(
        urlRequest: Requestable, method: String, headers: [String: String], body: Data?, timeout: TimeInterval, cachePolicy: URLRequest.CachePolicy, allowsCellularAccess: Bool, httpShouldHandleCookies: Bool, httpShouldUsePipelining: Bool, networkServiceType: URLRequest.NetworkServiceType, completion: @escaping (Result<Data, Error>) -> Void
    ) { 
        ...
    }
    

Anyway, can Swift currently match multiple types? Yes, but it's really cumbersome and hard to use, and that's the problem this proposal is trying to solve: Swift currently lacks a syntax that works well enough to match multiple types.

4 Likes