How to ensure specialized generic function is being used?

So I have the following 2 protocols:

protocol A {}
protocol B: A {}

And I specialize one of the functions for B, like this:

func asd<T: A>(_: T.Type) {}
func asd<T: B>(_: T.Type) {}

Other than this one function, I want everything else to work the same. But if I write another generic function, it will always call the first version and not the specialized one:

func qwe<T: A>(_: T.Type) {
    asd(T.self)
}

Is there any way I can use the specialized asd in this case without having to copy paste qwe with <T: B> generic parameter?

You current situation:

protocol A {}
protocol B: A {}

func asd<T: A>(_: T.Type) { print("asd<T: A>() called") }
func asd<T: B>(_: T.Type) { print("asd<T: B>() called") }

func qwe<T: A>(_: T.Type) { asd(T.self) }

struct SA : A {}
struct SB : B {}

func test() {
    qwe(SA.self) // asd<T: A>() called
    qwe(SB.self) // asd<T: A>() called
}
test()

This happens because the constraint on qwe's generic type parameter T is that it conforms to A (not B). That is, all the compiler can know about T is that it is some type that conforms to A, and it cannot just make a random guess that it might conform to B or C or whatever.

I'm not sure if the following would work in your actual use case but it's what I did in what I think might be a similar scenario:

protocol A {
    static func asd()
}
protocol B: A {}

extension A {
    static func asd() { print("A.asd() called") }
}
extension B {
    static func asd() { print("B.asd() called") }
}

func qwe<T: A>(_: T.Type) { T.asd() }

struct SA : A {}
struct SB : B {}

func test() {
    qwe(SA.self) // A.asd() called
    qwe(SB.self) // B.asd() called
}
test()

Or simpler, if what you want is specific functions for the concrete types SA and SB:

protocol A {
    static func asd()
}

struct SA : A {
    static func asd() { print("SA.asd() called") }
}
struct SB : A {
    static func asd() { print("SB.asd() called") }
}

func qwe<T: A>(_: T.Type) { T.asd() }

func test() {
    qwe(SA.self) // SA.asd() called
    qwe(SB.self) // SB.asd() called
}
test()
1 Like

This happens because the constraint on qwe 's generic type parameter T is that it conforms to A (not B ). That is, all the compiler can know about T is that it is some type that conforms to A , and it cannot just make a random guess that it might conform to B or C or whatever.

From my understanding, that would only be true if asd wasn't generic and instead had an A.Type parameter. (My actual code uses associated types for these protocols, so I couldn't even use non-generic functions.) And if you are using generics, the compiler knows exactly what type you are using.

For example changing the current example code like this:

func qwe(_ type: A.Type) { asd(type.self) }

You would get a compiler error because you tried to call a generic function with a non-concrete type.

This is my current use case: GitHub - Cyberbeni/TypedNotificationCenter: Typed version of Apple's NotificationCenter to avoid forgetting setting parameters in the userInfo dictionary and needing to handle not having those parameters.
While moving public functions to the other side of the equation would solve the issue, it still would require API change from the current one that is mirroring an Objective C API everywhere it can and it wouldn't 100% ensure that adding features in the future wouldn't have the same problem.

I guess this is a "bug" in the compiler and the only fix is to report it and hope it gets accepted as a bug and fixed in future Swift versions.

No, this is not a bug in the compiler. It is expected behavior, and you'll have to adjust your mental model to understand it.
: )
I'm afraid I cannot explain it better than in my above attempt but I'm sure others can.


This will not compile because you mention T without declaring it as a generic type parameter. (I don't see how that has anything to do with the question of the OP.)

If you write a function like this:

func zxc<T>(_: T.Type) { /* … */ }

Then the type paramter T is totally unconstrained (meaning nothing is known about it at compile time) and therefore it is essentially the same as:

func zxc(_: Any.Type) { /* … */ }

This will not compile because you mention T without declaring it as a generic type parameter.

My bad, this is the correct one that won't compile because T has to be known for calling asd:

func qwe(_ type: A.Type) { asd(type.self) }

This would compile if you had declared an overload of asd like the following (which takes the type of the protocol A, not a type conforming to the protocol A):

func asd(_: A.Type) { print("asd(_: A.Type) called") }

But again, I fail to see what it has to do with your original question.


I think what you need to understand is exemplified by this program:

func zxc<T>(_: T.Type) {
    // There is *runtime* info about `T` here, so we can print its type:
    print("Printing runtime type of `T` from zxc:\n ", T.self)
    // But at compile time, nothing is known about `T` here, because
    // `T` is totally unconstrained. So this:
    print("Calling qaz(T.self) from zxc:")
    qaz(T.self)
    // Will only be able to call a qaz for a totally unconstrained type.
    // So it will always, no matter the runtime type of T, be calling
    // qaz<T>(), and never eg qaz<T: Numeric>().
}

func qaz<T>(_: T.Type) { print("  qaz<T>()") }

func qaz<T: Numeric>(_: T.Type) { print("  qaz<T: Numeric>()") }

func test() {
    zxc(Int.self)
    print("Calling qaz(Int.self) from test:")
    qaz(Int.self)
}
test()

Which will print:

Printing runtime type of `T` from zxc:
  Int
Calling qaz(T.self) from zxc:
  qaz<T>()
Calling qaz(Int.self) from test:
  qaz<T: Numeric>()

Remember that Swift's generics is not like eg C++ templates.

So this compiles:

protocol A {}
protocol B: A {}

func asd<T: A>(_: T.Type) { print("asd<T: A>() called") }
func asd<T: B>(_: T.Type) { print("asd<T: B>() called") }

func qwe<T: A>(_ type: T.Type) { asd(type.self) }

But this doesn't:

protocol A {}
protocol B: A {}

func asd<T: A>(_: T.Type) { print("asd<T: A>() called") }
func asd<T: B>(_: T.Type) { print("asd<T: B>() called") }

func qwe(_ type: A.Type) { asd(type.self) }

Because the compiler has to know the type for every asd call, so it knows which specialized methods to generate from that 1 generic method.

See my reply above. I cannot explain it clearer.

Again, the only thing the compiler can know about a generic type parameter is what you have explicitly told it about that generic type parameter, via constraints.

It doesn't matter what the type of the arguments are at the call sites. It is not like C++ templates where each different call site result in a different version of the function.

func qwe<T   >(_: T.Type) { /* compiler knows nothing about T */ }
func qwe<T: A>(_: T.Type) { /* compiler knows that T conforms to A */ }
func qwe<T: B>(_: T.Type) { /* compiler knows that T conforms to B */ }

Swift's generics are a compile time feature, and what it knows at compile time about a certain function's T is only decided by the explicit constraints on that T. Code that calls those functions does not matter at all for what's knowable at compile time within the function definitions.

4 Likes
struct TypeErased {
    let block: () -> ()
}

protocol A {
    static func eraseType() -> TypeErased
}
protocol B: A {}
extension A {
    static func eraseType() -> TypeErased {
        TypeErased {
            asd(Self.self)
        }
    }
}
extension B {
    static func eraseType() -> TypeErased {
        TypeErased {
            asd(Self.self)
        }
    }
}

func asd<T: A>(_: T.Type) { print("asd<T: A>() called") }
func asd<T: B>(_: T.Type) { print("asd<T: B>() called") }

func qwe<T: A>(_ type: T.Type) {
    if T.self is B.Type {
        print("T is B but calling asd with it results in compiler error")
        T.eraseType().block()
    } else {
        asd(type.self)
    }
}

class SB : B {}

qwe(SB.self)

This will correctly call asd<T: B>(_:) but the problem is that

if T.self is B.Type

Only works if B doesn't have associated types. And in my real use case, it has, so basically I can keep this public API but I always have to use type erasure to then call back to my real function instead of just when the wrong function is called.

Hopefully that plus one layer of calls won't affect performance.

In Swift, the static type (at compile time) is not the same as the dynamic type (at run time). Swift generics are a static feature, and type casting using is, as? or as! is a dynamic operation. I find a good rule of thumb is that mixing generics and dynamic casts is to be avoided.

Without knowing the specifics, it’s hard to say, but based on your initial question it looks like you want a dynamically dispatched asd. For that it needs to be a protocol requirement spelled as a method.

5 Likes

Assuming the free generic asd functions must be the way they are, and should be called via another free qwe function that is the way it is, and assuming that you can add requirements and default implementations on A and B, then you can wrap the asd functions within default implementations of a requirement like this:

protocol A {
    static func callAsd()
}
protocol B: A {}

func asd<T: A>(_: T.Type) { print("asd<T: A>() called") }
func asd<T: B>(_: T.Type) { print("asd<T: B>() called") }

extension A {
    static func callAsd() {
        asd(self) // <-- Compiler chooses asd<T: A> overload here.
    }
}
extension B {
    static func callAsd() {
        asd(self) // <-- Compiler chooses asd<T: B> overload here.
    }
}

func qwe<T: A>(_: T.Type) { T.callAsd() }

struct SA : A {}
struct SB : B {}

func test() {
    qwe(SA.self) // asd<T: A>() called
    qwe(SB.self) // asd<T: B>() called
}
test()

If you want to get dynamic dispatch, you'll need the function to be either

  • protocol member, or
  • class member.

and if not, you can just put everything in A.callAsd and B.callAsd