Search code examples
swiftgenericsswift3protocols

Generic completion passed as non-generic


I'm working on some framework and faced a problem.

I have a public protocol:

public protocol MyPublicProtocol1 {
}

And another one, which contains a function with generic argument passed. Generic argument has a constraint – argument type must implement the first public protocol:

public protocol MyPublicProtocol2 {
    func someFunc<T: MyPublicProtocol1>(completion: (T) -> ())
}

Then I'm implementing my protocols not in public classes. Inside that function with generic argument I have to call another one that takes not generic argument and look like that:

func anotherFuncWith(completion: (MyPublicProtocol1) -> ())

And here's what implementation looks like:

class MyPublicProtocol1Impl: MyPublicProtocol1 {
}

class MyPublicProtocol2Impl: MyPublicProtocol2 {
    func someFunc<T: MyPublicProtocol1>(completion: (T) -> ()) {
        anotherFuncWith(completion: completion)
    }
}

And of course I have an error in the last string.

I can't declare someFunc(completion:) with not a generic argument like:

func someFunc(completion: (MyPublicProtocol1Impl) -> ())

Because MyPublicProtocol1Impl class mustn't be public. And I also can't declare anotherFuncWith(completion:) to take generic argument too for some reasons.

Is there a way to somewhat "convert" (T: MyPublicProtocol1) -> () completion to be just a (MyPublicProtocol1) -> ()?

Any help or advices are very appreciated! And thank you for reading my story!


Solution

  • You've asked for something that is not provably true. You have a method:

    func anotherFuncWith(completion: (MyPublicProtocol1) -> ())
    

    This accepts a method that can receive any MyPublicProtocol1. You then pass it a method of type:

    (T: MyPublicProtocol1) -> ()
    

    anotherFuncWith may pass something that is not T, at which point this is undefined. To make it more concrete, let's get rid of most of the stuff here and make MyPublicProtocol1 be Any (just to pick a trivial protocol).

    func anotherFuncWith(completion: (Any) -> ()) {
        completion("This is a string which is an Any, so that's fine")
    }
    
    func someFunc<T: Any>(completion: (T) -> ()) {
        anotherFuncWith(completion: completion)
    }
    

    This fails to compile exactly like your example. Now let's think through what I could do if it did compile. I could call:

    func complete(x: Int) -> () {
        print(x + 1)
    }
    
    someFunc(completion: complete)
    

    So now anotherFuncWith calls complete passing a String, which can't be added. Crash.

    The underlying problem here is that you've gotten covariance and contravariance backwards.

    How do we fix it? Depends on what you really mean. This code is a little strange. Do you care about the actual type of T or not? You never seem to use it. If you don't care, then just use protocols:

    public protocol MyPublicProtocol2 {
        func someFunc(completion: (MyPublicProtocol1) -> ())
    }
    

    If you do care about the actual type, use a PAT:

    public protocol MyPublicProtocol2 {
        associatedtype T: MyPublicProtocol1
        func someFunc(completion: (T) -> ())
    }
    

    Or you may want to rethink whether you need a protocol here at all. I often find people reach for protocols when they don't need them yet. Do you have multiple implementations of these protocols? Do you have multiple types that are passed? If not, I'd simplify and only go generic/protocol when you have a real problem you're solving in the current code. (You may need them; this is just my stock advice that many people have found useful when they've over-designed.)