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:
-
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) ... }
- The available types are fixed, and can be iterated over with (
-
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 { }
- enum needs to implement an additional method to return a value with this commonality
-
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.