Search code examples
iosswiftmacostypesalamofire

Passing concrete instance to closure that expects a protocol parameter


In my app, I have a function to call a specific API endpoint of mine, and that function accepts a closure as a completion handler. That closure accepts a Result of my custom Decodable type (Category) in the success case, and an Error in the failure case. Altogether, its method signature looks like this:

static func getAllRestaurantCategories(completion: @escaping (Result<[Category], Error>) -> Void) -> Void

This function calls out to Alamofire to determine the languages that the server supports, and then get the list of all possible restaurant categories. It's implemented like this:

static func getAllRestaurantCategories(completion: @escaping (Result<[Category], Error>) -> Void) -> Void{
    API.localizedRequest(API.categories) { (request: DataRequest) in
        request.responseDecodable(of: [Category].self) { (response: DataResponse<[Category], AFError>) in
            completion(response.result)
        }
    }
}

However, on the line with completion(response.result), I get a compiler error that says Cannot convert value of type 'Result<[Category], AFError>' to expected argument type 'Result<[Category], Error>'. This error goes away if I change the closure my method accepts to accept an AFError in the failure case, like this:

static func getAllRestaurantCategories(completion: @escaping (Result<[Category], AFError>) -> Void) -> Void{
    API.localizedRequest(API.categories) { (request: DataRequest) in
        request.responseDecodable(of: [Category].self) { (response: DataResponse<[Category], AFError>) in
            completion(response.result)
        }
    }
}

Alamofire's AFError conforms to Error, so it seems to me that this should work just fine. I know I can parse Alamofire's Result myself and generate my own to pass to my completion handler, but I'd rather not write all that extra custom code if I don't have to. How can I get the type system to understand that this should be ok?


Solution

  • Simply put, in (at least the current version of) Swift, if Sub is a subtype of Base, that doesn't mean that Container<Sub> is a subtype of Container<Base>.

    In fact, Container<Sub> and Container<Base> are unrelated types.

    So, while we can do the following:

    protocol Car {}
    struct Toyota: Car {}
    
    let a: Car = Toyota()
    

    but we can't generally (with notable exception of Swift's standard library collection types) do this:

    struct Container<T> {}
    
    let c: Container<Car> = Container<Toyota>() // ERROR
    

    It is said that Container<Car> and Container<Toyota> are not covariant


    Result has a mapError function that should make it fairly painless:

    completion(response.result.mapError { $0 as Error } )