iosswiftgenericsswiftui

Shadowing functions/properties using generics works outside the object but not inside it?


I have a generic type (Bar) that shadows the implementation of the function foo depending on T:

struct Bar<T> {
    func foo() {
        print("foo generic")
    }

    func foo() where T == String {
        print("foo string")
    }
}

When I call it, it works like I expect it to:

B<Double>().foo() // foo generic
B<String>().foo() // foo string

Now I create an another function that calls foo inside the object:

func boo() { foo() }

But when I call boo, it doesn't use the string "shadow" altogether.

B<String>().boo() // foo generic

Things I tried:

struct Bar<T> {
    let inner: Far<T>

    func boo() {
        inner.foo()
    }
}

struct Far<T> {
    func foo() {
        print("foo generic")
    }

    func foo() where T == String {
        print("foo string")
    }
}

Notice:

I ran into this issue while creating a SwiftUI body. So I'm unable to use some solutions.

// Very simplified.

struct MyView<APIClient: SomeClientProtocol>: View {
    let client: APIClient

    var body: some View {
        Button("Fetch") {
            Task(operation: fetchData) // Always calls generic
        }
    }

    func fetchData() async {
        await client.fetch()
    }

    func fetchData() async where APIClient: FetchSpecially {
        await client.specialFetch()
    }
}

Solution

  • In your first example, boo doesn't have a specialized overload, this is why the compiler always dispatches to the the generic overload. There is only one implementation for boo, and since for generics the dispatch decisions take time at compile time, you end up with boo always calling the non-specialized overload of foo.

    If you were to add a specialized func boo() where T == String { foo() }, you'll see that the desired foo implementation is called. At this point the compiler generates two implementations for boo, and the callers of boo can pick the specialized one that will forward the call to the specialized foo. However this is not a scalable approach, as adding new specializations of the foo method will have to be carefully synced with boo, there is no help from the compiler here.

    The same happens in the second example, the body property of the SwiftUI view doesn't operate on any specializations, this is why it always dispatches to the general, non-specialized fetch function. If it were a function you could've also specialize it for the more specific protocol, but that might've led to code duplication.

    For your particular case the solution is to construct the Task closure at initialization time, this way the compiler knows where to dispatch:

    struct MyView<APIClient: SomeClientProtocol>: View {
        private let fetchData: @MainActor () async -> Void
        
        init(client: APIClient) {
            fetchData = {
                await client.fetch()
            }
        }
        
        init(client: FetchSpecially) {
            fetchData = {
                await client.specialFetch()
            }
        }
    
        var body: some View {
            Button("Fetch") {
                Task(operation: fetchData) // Calls generic or specialized, depending on how the view was instantiated
            }
        }
    }
    

    With the above setup in place, the compiler will pick the appropriate initializer, assuming you don't indirectly pass the client as the base protocol - i.e. the call site knows if the client is an FetchSpecially or not.