Variadic Generics

A Sequence doesn’t necessarily have a consistent ordering. It has only whatever comes next.

Your Swift 5.6 examples concern protocols/existentials, it's not related to generics, except indirectly.

The pitch says awaiting implementation. Is someone yet to start implementing it, or is there a WIP implementation that I could play around with? I don't mind how experimental, incomplete or dodgy the implementation is.

1 Like

The implementation is on pause. Some of the type-checking is in main, but I think it doesn't match the current pitch, and it definitely doesn't match what we intend to propose. More importantly, it's just the type-checking; there's no code generation at all. We do have some idea of what the underlying ABI will be, though.

8 Likes

Ok, thanks. Is there some sort of public timeline for it? Or is it more of a far away feature?

1 Like

We’d like it as soon as possible, but I can safely say it won’t be in 5.7.

13 Likes

I just realized this works today (Swift 5.6)

func variadic(_ values: (any CustomStringConvertible)...) {
    for value in values {
        use(value)
    }
}

func use(_ value: some CustomStringConvertible) {
    print(String(describing: value), type(of: value))
}

variadic(1, "string", true) 

prints

1 Int
string String
true Bool

It might work for some of the examples that are brought up in the Motivation section of the original proposal.

1 Like

That doesn't seem to work on my machine.

I'm using Xcode 14 beta. Must be Swift 5.7 then

I don't see how it would, either: you're erasing the type information in the first function, then magically reading it in the second.

That magic is SE-0352

2 Likes

Yeah that’s 5.7 stuff.

What surprised me is that the runtime picked the first (generic) use function to print the value, even though I had overrides with more specific, statically typed variants.

func use(_ value: some CustomStringConvertible) {
    print(String(describing: value), type(of: value))
}

func use(_ value: String) {
    print(value, "it's a String")
}

func use(_ value: Int) {
    print(value, "it's an Int")
}

func use(_ value: Bool) {
    print(value, "it's a Bool")
}
1 Like

That is how Swift generics always work. Overloads (and protocol requirement implementations) are resolved based on the type information locally available at the use site; they're not like C++ templates where the code is instantiated with the type argument substituted in and then everything is re-resolved with that knowledge.

8 Likes

Function specializations are resolved at compile-time, so if a type of the value passed is unknown at the compile-time, generic function would be used.

So you can't specialize generic functions for any types. Instead, you could define desired specializations by using protocols.

protocol P: CustomStringConvertible {
    func use()
}

extension P {
    func use() {
        print(String(describing: self), type(of: self))
    }
}

extension Int: P {
    func use() {
        print(self, "it's an Int")
    }
}

(0).use() // 0 it's an Int
(0 as P).use() // 0 it's an Int

1 Like

In this aspect they're exactly like C++ templates – those are also resolved at compile-time so specializations won't be called there as well.

struct A {

};

struct B : public A {

};

template<typename T>
void use(T value) {
    std::cout << "generic function" << std::endl;
}

void use(B value) {
    std::cout << "special case" << std::endl;
}

int main(int argc, const char * argv[]) {
    use(B()); // special case
    use((A) B()); // generic function
    return 0;
}

"Compile time" is not an adequate summary here. Swift generics resolve based on the static type information available to the generic definition. C++ templates resolve based on the static type information available to the template after the substitution of template arguments, which are (when inferred for a function template) taken from the actual static types of the arguments. Those are very different behaviors; for example, Laszlo's example would call the concrete overloads of use if Swift used the C++ model.

You're right that the C++ model is still based on static type information; if an expression produces a pointer or reference to a base class of the true dynamic type, C++ will resolve overloads and infer template arguments based on that base class and not on the dynamic type. Accuracy demands that I point out that that's not what's happening in your example, though; the argument here is an object slice produced by copying the A base subobject out of the temporary B, and its dynamic type truly is A.

10 Likes

Would it? I think that's only true if the variadic function actually used variadic generics as pitched. By using the any CustomStringConvertible the static type information link is broken—even if Swift used a C++-style model it seems to me like the generic use declaration would end up being called, rather than the specialized overloads. But we're talking hypotheticals on hypotheticals at that point. :sweat_smile:

3 Likes

You're right, I was mentally mapping it over to a variadic template. C++ doesn't have any way to declare a function that takes a variadic number of concretely-typed arguments, so you would have to do something different here.

2 Likes

Can you provide a link (for example, a post in the forum) or give a quick explanation about why it was decided for Swift generics to work this way? I have a general knowledge about the difference, but I never quite explored the reasoning behind it.

1 Like