Nested generic trouble

In the following app:

protocol P {}
struct S: P {}

func foo<T: P>(_ value: T) {
    print(type(of: value)) // S
    print(value) // S()
    bar(value)
}

func bar(_ value: S) { print("S call") }

func bar<T: P>(_ value: T) { print("P call") }

func test() {
    let s = S()
    bar(s) // S call
    foo(s) // P call
}

calling bar directly prints expected "S call", but calling it via "foo" prints unexpected "P call". Or is that ok and my expectations are wrong?

Edit: Interestingly it works as expected in C++
struct S {};

void bar(S) {
    printf("S call\n");
}

template <typename P>
void bar(P) {
    printf("P call\n");
}

template <typename P>
void foo(P p) {
    bar(p);
}

void cppTest() {
    S s;
    bar(s); // S call
    foo(s); // S call
}

https://developer.apple.com/forums/thread/7372?answerId=17654022#17654022

Interesting that C++ can do this but not Swift.

I just tracked the 10x slowdown in my barebones codable implementation because of this feature; put a workaround and now it's just one order of magnitude slower than the manual serialiser instead of two.

It's not really a case of "can do that" vs "can't do that". It's totally different semantic model. Templates and generics are two different things that happen to be spelled kinda similarly, and using one and expecting it to behave like the other will result in pain, as you have found (trying to use a template when the actual type cannot be known at compile time is equally painful, for example).

10 Likes

I'd not call that "pain", just I was surprised.

struct S {}
func bar(_ value: S) { print("S call") }
func bar<T>(_ value: T) { print("T call") }
func foo<T>(_ value: T) { bar(value) }

func swiftTest() {
    let s = S()
    bar(s) // S call
    foo(s) // T call
}
// C++ -----------
struct S {};
void bar(S) { printf("S call\n"); }
template <typename T> void bar(T) { printf("T call\n"); }
template <typename T> void foo(T t) { bar(t); }

void cppTest() {
    S s;
    bar(s); // S call
    foo(s); // S call
}

That the two are so similarly looking (as in the example above) caught me by surprise, I genuinely thought they are the same or very similar things. I wonder why the syntax for the two totally different semantic models was chosen to be so similar.

Somewhat glibly, because < > is the only single-character delimiter-looking thing that's otherwise mostly unused and available in most standard keyboard layouts.

I suppose we could have done something like func foo/T,U,V/(t:T, u:U) -> V, or func foo(T,U,V) -> (t:T, u:U) -> V or ...

Another time, another language!

1 Like

Right.
I am not suggesting the change, but wonder could Swift have chosen to use C++ template model (when that design decision was made) or was there something in Swift that prevented it?

Not something "in Swift" but it would have been contrary to the goals of the language. From the introductory material of @Slava_Pestov's generics document:

Swift generics were designed with four primary goals in mind:

  • Generic definitions should be independently type checked, without knowledge of all possible concrete type substitutions that they are invoked with.
  • Shared libraries that export generic definitions should be able to evolve resiliently without requiring recompilation of clients.
  • ...

The C++ template model does not satisfy the first two goals, which are critical for the task of distributing an SDK for binary-stable libraries shipped in the OS.

5 Likes

my take on this is that there has been quite a bit of convergent evolution and most real world uses of generics just end up lowering them to templates through hilarious overuse of @frozen and @inlinable. there really isn’t any other way to get C++ level performance with generics, you have to turn them into templates either way.

i do have a carveout to this strategy for generics constrained to AnyObject, since those are pretty cheap. but i would rarely leave a non-reference type constrained generic unspecialized.

2 Likes

I have struggled with this myself few years ago (coming from C++).

I would be really interested if you have a concrete example. Can I specify completely different implementation of a generic function for a given type parameter (essentially template specialisation in C++)?

Edit: My understanding of the difference between Swift generics and C++ Templates is, that Template is essentially a special macro, that always gets fully resolved at build time - since unlike Swift generics, all template code must be inside a public header file. And thus it can resolve a "specialisation" from within a context of another template.

yes, this can be used to add type safety for APIs that come in multiple "flavors". but that's not really the use case described in this thread, since you wouldn't need to inline those generics, since they aren't being used in the actual algorithm, they are only being used to gate an API.

1 Like

when you spray inlinable over a type that is effectively the swift equivalent of moving it into a header file.

the ugly secret of swift is we are hilariously dependent on inlining to accomplish even the most basic of things, and the only reason this isn't seen as a problem is because the situation in C++ is even worse.

this is the real reason why we in the swift world tend to write very large modules that contain many files and many types, whereas a C++ "module" usually consists of a single cc file with a single type and a single associated header file.

1 Like

I asked a similar question just the another day and the answer was 'it just doesn't work like that'. Essentially the concrete type info is stripped away as soon as its passed into a generic function.

I thought this to be correct.

OK, but unlike in C++, I can't just create a generic type and an overloaded function like

public struct Container<T: Encodable> {
  public var value: T
  @persisted var storage: Data

  public func bytes(_ val: Int) -> Data { /* return a bit pattern */ }
  public func bytes(_ val: String) -> Data { /* return a C String */ }
  public func bytes<A: Encodable>(_ val: A) -> Data { /* use JSONEncoder */ }

  public func store() {
    _storage = bytes(value)
  }
}

even if I use @inlinable, correct? I am aware, that @inlinable (and other things like open) emmit something (ast?) into the .swiftmodule file. But will this change the behaviour of the func store(), so it works as a C++ Templated type would?

i don't understand what you're asking here. all three methods would be available on every specialization of Container because you have not added any where constraints to any of them.

I don't think this is accurate. Swift has (modulo a couple of limitations that have not been mentioned in this thread) a "best of both worlds" situation, where it is simultaneously possible to a) write an ABI-stable generic function that can evolve without recompiling the application, and b) write a high-performance generic algorithm that is monomorphized into caller's code, depending on what the implementer requires.

I think a case can be made that non-resilient code (i.e. SwiftPM packages) could benefit from letting the compiler have much freer reign with exposing implementation details. But I don't think that's quite what you said here.

4 Likes

@inlinable and open behave very differently.

@inlinable doesn't emit AST, it literally emits source code.

4 Likes

in C++ you do the "stable generic function" thing with class inheritance, so it's not quite that swift generics are more powerful than C++ templates, it's just that swift has combined the two features into one, and inlinability is how you select between the two subfeatures.

on the other hand @inlinable is way more widespread in swift code than you would expect in similarly written C++ code, because we copy everything by default in swift, whereas we usually pass-by-reference in C++. and a lot of the time the @inlinable in swift serves no purpose other than to expose the copy constructor. and that is why i think C/C++ people are sometimes surprised by how much inlining swift uses.

Essentially, my point was, that I've made assumption in the past, that Swift generics work like C++ template and since following C++ program has such output:

#include <iostream>

template<typename T>
struct Container {
    public:
    T value;

    // Templated function bytes
  template <typename A>
  void bytes(A arg) {
    std::cout << "A" << std::endl;
  }
  
  void bytes(std::string arg) {
    std::cout << "std::string" << std::endl;
  }

  void bytes(int arg) {
    std::cout << "int" << std::endl;
  }

  void store() {
    bytes(value);
  }
};

int main() {
    Container<int> inst;
    inst.store(); // prints "int"
    std::string str = "Hello";
    inst.bytes(str); // prints "std::string"
    return 0;
}

the Swift program should have output "Int" and "String" for this program

public struct Container<T: Encodable> {
  public var value: T

  public func bytes(_ val: Int) { print("Int") }
  public func bytes(_ val: String) { print("String") }
  public func bytes<A: Encodable>(_ val: A) { print("A") }

  public func store() {
    bytes(value)
  }
}

let inst = Container<Int>(value: 10)
inst.store() // prints "A"
inst.bytes("hi") // prints "String"

and I was confused, since I thought you were saying, that I can change this behaviour by suing @inlinable.

I would be interested in any performance analysis you are able to share that informed the decision to lean heavily on @inlinable.

1 Like