0

I'm seeing some strange behavior at the interface of protocol extensions and generics. I'm new to Swift, so possibly I misunderstand, but I don't see how this can be correct behavior.

First let's define a protocol, and extend it with a default function implementation:

protocol Foo {
}

extension Foo {
    static func yo() {
        print("Foo.yo")    
    }
}

Now define a couple of conforming types:

struct A: Foo {
}

struct B: Foo {
    static func yo() {
        print("B.yo")
    }
}

A.yo()
B.yo()

As expected, A.yo() uses the default implementation of yo, whereas B.yo() uses the explicit implementation provided by B: the output is

Foo.yo
B.yo

Now let's make a simple generic type:

struct C<T: Foo> {
    static func what() {
        T.yo()
    }
}

C<A>.what()
C<B>.what()

C<A>.what() prints Foo.yo, as expected. But C<B>.what() also prints Foo.yo!

Surely the meaning of C<B> is simply the template for C with B substituted in for the type parameter T? Yet B's version of yo is not being called.

What am I missing? I'm using Swift 5.2.2.

Now, as it turns out you can fix this problem by declaring yo in the original definition of Foo. If we do this:

protocol Foo {
    static func yo()
}

then C<B>.what() works as I would expect, printing B.yo. I can't understand the original behavior in the first place, but even less can I understand how this would change it.

In my actual application I can't use this fix, because I am extending a pre-existing protocol with a function that I want to specialize in a particular conforming type.

Bob Hearn
  • 9
  • 4
  • 1
    Generics are resolved at compile time. They're not dynamically dispatched like method calls on class hierarchies or protocols. The staticness is kind of their point, that's where the performance wins stem from. You should make `yo` a requirement of the protocol, and then it'll be dynamically dispatched. – Alexander May 12 '20 at 22:48
  • Yes I understand. But surely C should be resolved at compile time to call B.yo in its what method? – Bob Hearn May 12 '20 at 22:50
  • My comment was getting to long, I wrote up a full answer. Hopefully that helps – Alexander May 12 '20 at 22:55
  • It might be that the generic is irrelevant and this is a case of https://stackoverflow.com/questions/31431753/swift-protocol-extensions-overriding but I am not going to worry about that. – matt May 12 '20 at 23:25
  • @matt yes that does seem similar, except in that case it's clearly an issue of static vs. dynamic dispatch: here I am still missing how that comes into it. It appears to me to be a matter of the semantics of `struct C`, where `T` is interpreted as `Foo` in the generic body, rather than simply limiting this generic to `T`'s which satisfy `Foo`, as I would have expected. – Bob Hearn May 12 '20 at 23:40

2 Answers2

2

Generics are resolved at compile time. They're not dynamically dispatched like method calls on class hierarchies or protocols. That staticness is kind of their point, that's where the performance wins stem from.

As far as I can tell, Foo.yo() and B.yo() are totally unrelated functions. Calling Foo.yo() does a statically dispatched call to Foo, and likewise, calling B.yo() causes a statically dispatched call to B.

Yet, if you up-cast B.self to a Foo.Type, and you call yo() on it, you end up with a statically dispatched call to Foo:

(B.self as Foo.Type).yo()

To get dynamic dispatch (to achieve the kind of polymorphism you're after), you need to define yo as a requirement of the protocol. That establishes a relationship between B.yo() (which is now a part of the conformance to the protocol) and Foo.yo() (which is a default implementation for types who don't provide their own).

protocol Foo {
//  static func yo() // uncomment this
}

extension Foo {
    static func yo() {
        print("Foo.yo")    
    }
}

struct A: Foo {
}

struct B: Foo {
    static func yo() {
        print("B.yo")
    }
}

struct C<T: Foo> {
    static func what() {
        T.yo()
    }
}
A.yo()
B.yo()
(B.self as Foo.Type).yo()
C<A>.what()
C<B>.what()

Results before:

Foo.yo
B.yo
Foo.yo
Foo.yo
Foo.yo

Results after making yo a requirement:

Foo.yo
B.yo
B.yo
Foo.yo
B.yo
Alexander
  • 59,041
  • 12
  • 98
  • 151
  • Thank you. I think I understand. When I say ```struct C``` the compiler is treating type variable `T` in a way similar to an up-cast. I still don't really see how I am expecting dynamic dispatch, though. The compiler knows that B has a `yo` method, and can generate the appropriate static call at compile time. – Bob Hearn May 12 '20 at 23:15
  • Perhaps the problem here is that I am coming at this from a C++ programmer's perspective on generics, which are simply templates that are instantiated with particular instances of type variables at compile time. Apparently Swift generics don't work the same way. – Bob Hearn May 12 '20 at 23:50
  • @BobHearn C++ templates are rather "dumb", in that they only duck-type based on the names of the symbols, operators, etc. They work completely unlike other languages I know (Java, C#, Ruby, Python and Swift), so I wouldn't try to carry over too many expectations, for fear of confusion. I have a rough understanding as to why this is happening, although It's really difficult to put into words, so bear with me. – Alexander May 13 '20 at 00:23
  • First, try adding a new protocol `protocol Ancestor {}`, and make `Foo` require `Ancestor` by changing its declaration to `protocol Foo: Ancestor {}`. When you try to run the code again, you'll get `error: type 'T' has no member 'yo'`. – Alexander May 13 '20 at 00:24
  • Swift's generics have two main modes of operation. Suppose we have a `List`. The first mode of operation is compile-time specialization, which works kind of similar to how templates work in C++. The definition of `List` is used to stamp out specializations like `List`, `List`, etc. One for every set of generic args necessary in the compiled program. As you might be familiar from C++, this works great with local code, but doesn't work when you try to vend a generic type as a library... – Alexander May 13 '20 at 00:27
  • ...In that case, the user might try to use the generic type as `List`. Because your library didn't produce such a specialization when it was being compiled, it doesn't exist for the client to be able to use it. To solve this problem, C++ templates are forced to share their source, so the client could stamp out their own specializations from source. This has its own set of problems (generic types must be open sourced, compile time slow-downs, etc.). Swift takes a different approach. It has a second mode of operation, in which a generic type is emitted as such into a binary – Alexander May 13 '20 at 00:29
  • The generic type is compiled so it operates on a set of boxes (called value witness tables, IIRC), which are produced by the client. These boxes define how a value of a client-defined type can be initialized, deinitialized, copied, moved, access its members, etc. The generic code can them operate on those boxes opaquely, calling the functions within to help it do its work. Importantly, this dynamic implementation of generics *still* doesn't do dynamic dispatch. If you try to invoke a static member (and not through a protocol or inheritance hierarchy), the runtime machinery won't try to... – Alexander May 13 '20 at 00:32
  • ... look up if the type contains the member that's being invoked. That polymorphism is still solely exposed through protocol conformances and subclasses. These two implementations of generics have trade offs (the first is generally way faster, but larger code size, the latter is slimmer, slower but more versatile), but they always have the same semantics. In your case, the compiler could easily see that `B` has a `yo()`, and could invoke it. But I don't think that's possible to do in the dynamic case, so it's not done. That's the best explanation I can think of. – Alexander May 13 '20 at 00:35
  • Thanks, that's very helpful. I will have to wrap my brain around generics that can be pre-compiled. It does make sense. However, when I try your `protocol Ancestor ...` experiment it works fine for me, no error. – Bob Hearn May 13 '20 at 00:38
  • "I will have to wrap my brain around generics that can be pre-compiled" haha it's not simple. Swift is the only language I'm aware of that can do that, and perhaps C# (though I haven't looked into detail. In C++ you're forced to share source, and Java sidetracks the problem entirely by just boiling all type-erasing all generics down to `Object` with implicit casts. Also it's funny that I mentioned Ruby and Python earlier, (as the languages I'm intimately familiar with) that I forgot that I was talking about something they don't even do (compile-time anything, let alone generics) – Alexander May 13 '20 at 00:45
  • Oh I forgot to tell you to change the generic type constraint on `T` to be `C`. Here's the full code: https://gist.github.com/amomchilov/05ef1381f3c715a6143359420970a61b – Alexander May 13 '20 at 00:46
0

It’s hard to suggest a fix for your exact situation without more details of the exact situation- are you not able to provide these? Suffice to say this is the expected behaviour and its to do with some optimisations and assumptions the compiler makes.

You might want to check out this article on static vs dynamic dispatch in Swift: https://medium.com/@PavloShadov/https-medium-com-pavloshadov-swift-protocols-magic-of-dynamic-static-methods-dispatches-dfe0e0c85509

  • 1
    Thank you; the link is helpful. I was able to find a workaround for my particular situation. I was just so shocked by this behavior that I needed to understand it. – Bob Hearn May 12 '20 at 23:18