Overloading generic functions in iOS Swift

Viewed 320

I am trying to build a finder that tries and finds multiple types that are being passed to it inside a closure.

enum SomeError: Error {
    case notInitialized
}

struct TestFinder {

    func getSomething<T, U>(_ function: @escaping (T) -> U) throws -> U {
        guard let t: T = get() else {
                throw SomeError.notInitialized
        }

        return function(t)
    }

    func getSomething<T, U, V>(_ function: @escaping (T, U) -> V) throws -> V {
        guard let t: T = get(), let u: U = get() else {
                throw SomeError.notInitialized
        }

        return function(t, u)
    }

    func getSomething<T, U, V, W>(_ function: @escaping (T, U, V) -> W) throws -> W {
        guard let t: T = get(), let u: U = get(), let v: V = get() else {
                throw SomeError.notInitialized
        }

        return function(t, u, v)
    }

    private func get<T>() -> T? {
       nil
    }
}

struct UserDetails {
    let name: String
    let roll: String
}

I call the finder as:

let testReturnType = try? TestFinder().getSomething(UserDetails.init)

Compiler throws me an error of:

Ambiguous use of 'getSomething'

Reason for this error (from docs):

You can overload a generic function or initializer by providing different constraints, requirements, or both on the type parameters. When you call an overloaded generic function or initializer, the compiler uses these constraints to resolve which overloaded function or initializer to invoke.

But if I comment:

func getSomething<T, U>(_ function: @escaping (T) -> U) throws -> U

Everything starts working. It has something to do with the compiler not able to identify which function signature to resolve.

Any particular solution for this?

2 Answers

You haven't quite focussed on the actual issue. Let's eliminate everything irrelevant from the example. This compiles and works as expected:

struct TestFinder {

    func doSomething<T,U>(_ function: (T,U) -> Void) -> Void {
        print("two")
    }

    func doSomething<T,U,V>(_ function: (T,U,V) -> Void) -> Void {
        print("three")
    }

    func doSomething<T,U,V,W>(_ function: (T,U,V,W) -> Void) -> Void {
        print("four")
    }

}

And here we'll test it:

    func f(_ s1: String, _ s2: String, _ s3: String, _ s4: String) -> Void {}
    TestFinder().doSomething(f) // "four"

But if you add the version with one passed function parameter, everything breaks down:

struct TestFinder {

    func doSomething<T>(_ function: (T) -> Void) -> Void {
        print("one")
    }

    func doSomething<T,U>(_ function: (T,U) -> Void) -> Void {
        print("two")
    }

    func doSomething<T,U,V>(_ function: (T,U,V) -> Void) -> Void {
        print("three")
    }

    func doSomething<T,U,V,W>(_ function: (T,U,V,W) -> Void) -> Void {
        print("four")
    }
}

Now we can't compile, because the first version is seen as a candidate. And indeed, if we remove the other versions, we still compile!

struct TestFinder {

    func doSomething<T>(_ function: (T) -> Void) -> Void {
        print("one")
    }

}

That's the weird part. We still compile, even though we are saying:

    func f(_ s1: String, _ s2: String, _ s3: String, _ s4: String) -> Void {}
    TestFinder().doSomething(f)

Evidently, this function with four parameters is seen by the compiler as "fitting" the declaration with just one generic parameter.

I regard this as a bug. I think I can guess what might cause it; it could have to do with the legacy of function parameter list as tuples. This function f is "equivalent" to a function taking a single parameter consisting of a four-string tuple. Nevertheless, you cannot actually call the function inside doSomething with a four-string tuple; I cannot find a way to call it at all.

So, I would say, regard this as a bug, and work around it for now by removing the first version of your generic.


UPDATE: On the advice of the Swift team, I tested with the May 4, 2020 Swift 5.3 Development toolchain. With it, your code compiles and behaves as expected. This was indeed a bug, and it was fixed as part of

https://bugs.swift.org/browse/SR-8563

Returning for a moment to my version, my code, too, compiles and behaves as expected, with all four versions of doSomething present. However, note that if you delete all but the first version of doSomething, it still compiles and runs. Moreover, you can call function with four parameters by bundling them into a tuple and force casting, like this:

struct TestFinder2 {

    func doSomething<T>(_ function: (T) -> Void) -> Void {
        print("one")
        function(("manny", "moe", "jack", "henry") as! T)
    }

}

That seems to confirm my guess that what you're seeing is a consequence of the hidden tuple-nature of a function's parameter list. One can draw the same conclusion from the discussion of the bug, which refers to "tuple-splatting".

The key point is the UserDetails struct, because this struct has two properties and without any designed initializer, the initializer could be UserDetails(name: , roll: ) or UserDetails(name: ) or UserDetails(roll: ), this is the ambitious part. if you just delete one property of UserDetails that will work too, because one property struct has only one designed initializer.

If you comment

func getSomething<T, U>(_ function: @escaping (T) -> U) throws -> U

Which means the finder has only one chosen:

func getSomething<T,U,V>(_ function: @escaping(T,U) -> V) throws -> V
Related