Search code examples
swiftgenericsswift-extensionstype-constraints

Swift extension type constraints for generic class where generic type is yet another generic type


I'm trying to wrap my head around generic type constraints in Swift. Here is my starting point:

class Promise<T> {
    func resolve(_ block:@escaping (T) ->Void) {}
    func fulfill(_ result:T) {}
}

A Promise is just that, something that can be fulfilled in the future. This becomes very useful when it is used with Swift's Result type when handing results back from a background queue to the main queue:

let promise = Promise<Result<String, Error>>()
promise.fulfill(.success("Hello"))
promise.fulfill(.failure(NSError()))

Now I would like to add an extension to all Promise instances that use Result to add these helper methods:

extension Promise where T == Result<X, Error> {
                                    ⬆︎ Here's the problem ⚡️
    func failure(_ error:Error) {
        fulfill(.failure(error))
    }

    func success(_ result:X) {
        fulfill(.success(result))
    }
}

// Shorter:
let promise = Promise<Result<String, Error>>()
promise.success("Hello")
promise.failure(NSError())

Only problem is that the above code does not compile, because X is not defined. What I want to express is this:

Extend Promise when its generic type T is of type Result<X,Z> where X can be anything and Z must be of type ErrorResult<*, Error>. Is this possible?


Solution

  • What you want is possible, it's just that the syntax is slightly verbose. You can't put the where constraint on the extension. You have to put it on each method.

    extension Promise {
    
        func failure<U>(_ error: Error) where T == Result<U, Error> {
            fulfill(.failure(error))
        }
    
        func success<U>(_ result: U) where T == Result<U, Error> {
            fulfill(.success(result))
        }
    }
    

    You can also do it with a protocol, but for enums I find this very klunky, because enum cases can't be treated as conforming methods.

    protocol ResultType {
        associatedtype Success
        associatedtype Failure: Error
        static func makeFailure(_: Failure) -> Self
        static func makeSuccess(_: Success) -> Self
    }
    
    extension Result: ResultType {
        static func makeFailure(_ failure: Failure) -> Result<Success, Failure> { .failure(failure)  }
        static func makeSuccess(_ success: Success) -> Result<Success, Failure> { .success(success) }
    }
    
    extension Promise where T: ResultType {
        func failure(_ error: T.Failure) {
            fulfill(T.makeFailure(error))
        }
    
        func success(_ result: T.Success) {
            fulfill(T.makeSuccess(result))
        }
    }