-2

I'm using the swift Result<> generic in a function completion block, but passing it to another completion block with a protocol type doesn't work as expected.

public protocol Journey { }
class CommsJourney: Journey { }

typealias Completion = (Result<[Journey]?, Error>) -> Void
typealias CommsCompletion = (Result<[CommsJourney]?, Error>) -> Void

class Comms {
    func refreshTimes(completion: @escaping CommsCompletion) {
        
        let array = [CommsJourney()]
        completion(.success(array))
    }
}

/**
 Completion block returns `Journey` protocol if successful
 */
func refreshTimes(completion: @escaping Completion) {
    
    // Passing the previous completion block fails to compile as signatures are different
//    Comms().refreshTimes(completion: completion)
    
    Comms().refreshTimes { (result) in
        
        switch result {
        case .success(let results):
            // result is `CommsCompletion` yet returns perfectly fine
            completion(.success(results))
        case .failure(let error):
            completion(.failure(error))
        }
    }
}

Cannot convert value of type 'Completion' (aka '(Result<Optional<Array>, Error>) -> ()') to expected argument type 'CommsCompletion' (aka '(Result<Optional<Array>, Error>) -> ()')

I'd like to understand why passing the completion doesn't work, yet sending the result does. And also what is the best way to correct this? Is my switch example the easiest/best solution?

EDIT ----

I've been pointed to this q/a Swift generic coercion misunderstanding which either goes completely over my head, or isn't specific to my case. As I can fix this by simply extracting the result and then forwarding it on it seems overkill to create an intermediate Any... object.

EDIT ----- Playground gist here https://gist.github.com/ddaddy/d58b648dbe82a1c63fe23541cc1aad40

Darren
  • 10,182
  • 20
  • 95
  • 162
  • I don't understand how you can be confused. :) A Completion is not a CommsCompletion. So you cannot call `refreshTimes` with a Completion when it expects a CommsCompletion. What's the hard part? I ask for a banana and you give me an elephant; that doesn't compile. Done. (In the one that works, it has nothing to do with the switch. You could delete the whole body and it would still compile. It's that you changed the signature so that `refreshTimes` expects a Completion. I ask for an elephant and you give me an elephant.) – matt Nov 10 '20 at 14:20
  • Ok, I understand that although they are the same protocol, they are different completion type signatures. I didn't write my (The one that works) clearly, so I edited it. I can successfully pass the completion result from one completion block type to the other. – Darren Nov 10 '20 at 14:39
  • So Journey is a protocol? — Could you please edit with _actual code_, not just excerpts that use names we have to guess at and fill in for ourselves? Provide a [mcve] please if you want actual assistance. – matt Nov 10 '20 at 14:43
  • Yes. I'm sorry I missed that. The actual code is part of a very large code base so I had to strip it right back for the question. I'm usually good with my details but must have had an off morning. – Darren Nov 10 '20 at 14:46
  • Well, I don't change any of what I said. You are saying, in `class Comms`, that you declare `func refreshTimes(completion: @escaping CommsCompletion) { }` and it doesn't compile. If you change CommsCompletion to Completion, it does. The type in the call to the function and the type in the declaration of that function must match. The _content_ of the function is irrelevant to the story. – matt Nov 10 '20 at 14:47
  • Also you say "if I use the following" but you do not show anything about where / how you "use" it. You keep not giving _actual code_. Reduce this whole thing to a simple playground or gist that one can actually compile (or fail to compile), not a bunch of disconnected excerpts, as I asked you to do, please. – matt Nov 10 '20 at 14:51
  • You are right, I have added a proper playground/gist. I guess I know that the completion signatures do not match, but simply passing the result after the switch works even though it's the same result. – Darren Nov 10 '20 at 15:06
  • Sure, I see. Because it is obviously fine to pass a `[CommsJourney]` where a `[Journey]` is expected. That's all you are doing in the one that "works". But that is not at all the same as trying to pass a _function_ of type Completion where a _function_ of type CommsCompletion is expected. Do you understand now? – matt Nov 10 '20 at 15:22

1 Answers1

1

Okay, so this does not compile:

public protocol Journey { }
class CommsJourney: Journey { }

typealias CommsCompletion = (Result<[CommsJourney]?, Error>) -> Void

class Comms {
    func refreshTimes(completion: @escaping CommsCompletion) { }
}

typealias Completion = (Result<[Journey]?, Error>) -> Void

func callerOfRefreshTimes(completion: @escaping Completion) {
    Comms().refreshTimes(completion: completion) // nope
}

That's because you are trying to pass a Completion function where a CommCompletion function is expected, and they are completely distinct function types, so there's a typological mismatch and the compiler complains.

To understand why your second example does work, let's simplify it. So, this compiles fine:

public protocol Journey { }
class CommsJourney: Journey { }

typealias CommsCompletion = (Result<[CommsJourney]?, Error>) -> Void

class Comms {
    func refreshTimes(completion: @escaping CommsCompletion) { }
}

typealias Completion = (Result<[Journey]?, Error>) -> Void

func callerOfRefreshTimes(completion: @escaping Completion) {        
    Comms().refreshTimes { _ in
        let r : Result<[Journey]?, Error> = .success([CommsJourney]()) // <--
        completion(r)
    }
}

All we are doing there is creating a Result of the correct type and passing it into the completion function call. It is true that we formed the Result by using an array of CommsJourney, not an array of Journey as required by the type of the Result, but this is polymorphism in action: you can supply an array of the subtype where an array of the supertype is expected.

So in the second code, we do use polymorphic substitution on just the array. But we do not use any polymorphic substitution elsewhere. In particular, the Result we are creating is typed exactly as the type expected as parameter by the completion function. You are doing that same thing in your code, but you are doing it through implicit typing so it is not so obvious.

And of course the fact that in your code the value result arrives as the parameter to the surrounding closure is just a red herring! The very first thing you do in your code is pull that Result apart and extract the array that is inside it. You then throw the original Result away!

So, you don't try to pass the wrong Result type, and you don't try to coerce a Result; instead, you just throw away the first Result and make a whole new Result (of the correct type) and pass that (which is why I wrote my code to demonstrate doing that).

matt
  • 515,959
  • 87
  • 875
  • 1,141