Starting to implement Variadic Generics

In case of Tuple<T...> I think we can rely on the stdlib internal capabilities and hide the inner storage from the user. For the end-user Tuple<T...> would conform to some kind of ExpressibleByTupleLiteral protocol to achieve the old tuple syntax. The inner storage tuple of Tuple struct does not need to be the swifts (T...) current tuple, which avoids the paradox.

1 Like

So you suggest that some magic could go on only with the Tuple type? Why not, this can be an acceptable trade-off.

This still leaves me with a doubt, let me clarify one thing just to be sure:

struct ZipSequence<Sequences... : Sequence> {
  // For what concerns `ZipSequence`, `Sequences` is a tuple of unknown arity (S1, S2, ..., Sn)
}
func zip<Sequences... : Sequence>() {
  // Also here, `Sequences` is a tuple of unknown arity (S1, S2, ..., Sn)
}

Simply by declaring a generic T to be variadic, I would like the type / function to see it as a tuple, and this is independent of any member declared to be of that type. So if we have this in the stdlib:

struct Tuple<T...> {
  // Here, the compiler would treat `T` as a tuple like it would do
  // for every other type, even if there are no explicit members and
  // magic storage is going on
}

part of the issue will still remain. Maybe I'm missing part of your reasoning?

I'm not sure we're on the same page, but for me Sequences... nor T... is a tuple. These are 'just' generic parameter lists of a 1 ... n range. You can create a tuple 'only' if you write either (Sequences...) or Tuple<Sequences...> (the former falls back to the latter as we already have it today with [T] fallback to Array<T>). Also a small note, an empty tuple aka. Void cannot be represented by Tuple<T...> unless we say that T... can also have the size zero, but that would likely create more issues than helping here.

1 Like

Ah yeah, that was the problem! To me T was already a tuple inside a VG context, but not to you. And when I said:

I was saying exactly what you intended, that T is a group of parameters and another syntax - like (T...) - is its "tuple expansion".

About T... allowing for empty parameters, I'd like it to be true, but I've not thought about the potential issues. Do you have something specific in mind?

Thanks for your feedback!

On the first glance I have no usability issues that I can foresee, but I could guess that it might potentially require more language support and would complicate the already complex feature set we're talking about here.

On the other hand some people asked for being able to extend Void or conform custom protocol to it, which would typealias Void = Tuple<> let them do. Furthermore I think we have to be carful on how ExpressibleByTupleLiteral would behave as not every type that can potentially conform to the protocol can or should be representable by an 'empty' tuple.

Being able to conform Void to protocols is really important (especially Equatable and Hashable).

Another edge case to keep in mind here is the fact that Swift doesn’t actually have single-element tuples. This will probably complicate the design of Tuple (as a nominal type). I even wonder if we would need to make Void a separate nominal type and have something like Tuple<First, Second, Rest...> in order to prevent formation of single-element tuples like Tuple<Int> (because (Int) is actually just Int in Swift). This may be a really bad idea, but I don’t have a better one off the top of my head.

1 Like

That is true but only because of the ambiguity the syntax has today.

// This should be just fine because it wouldn't be ambiguous for the compiler anymore.
let tuple: Tuple<Int> = (42)
print(tuple.0)

So my two cents are, keep the source compatibility but allow single element tuples in an unambiguous way like above.

1 Like

So you're saying we support single element nominal tuples, but the syntactic sugar for tuples would continue to ignore the single-element case? I suppose that might work and would certainly be more elegant.

If I got your right then yes. Speaking with an example:

_ = (42)               // Implicitly Int
_ = Tuple(42)          // Explicitly Tuple<Int>
_ = (42) as Tuple<Int> // Explicitly Tuple<Int>
_: Tuple<Int> = (42)   // Explicitly Tuple<Int>
_ = (label: 42)        // Tuple<label: Int>  or something like that
5 Likes

@DevAndArtist I was thinking about the type of an "instantiated" VG, and I realised I do not know how to express it without VG being a tuple all the way up to the declaration:

struct Bag<T> {
  init(_ items: T...) { }
}

// This is Bag<Int>
let bag1 = Bag(1, 2, 3)

// This is Bag<String>
let bag1 = Bag("Hello", "World")

// This is Bag<()> aka Bag<Void>
let bag1 = Bag(())

struct Variadic<S... : Sequence> {
  // Let's assume that `S...` here makes the initialiser variadic but
  // without the same-type restriction
  init(_ sequences: S...) {}
}

// What is the type of this? If VG are always treated as tuples, this
// could be Variadic<([Int], String)>, but what if we do not go down
// that route like we said in the previous posts?
let v1 = Variadic([1, 2, 3], "Hello")


Edit: I'm soooo dumb, maybe I should wait a little bit after work before writing :no_mouth: the type could simply be Variadic<[Int], String>, right?

1 Like

My mental model is simple as again it‘s 'just' a comma sparated list of 1...n generic type parameters.

_: Tuple<Int, String, Bool> = (42, "Swift", true)

Your type signature is also valid, but then you need to make it to a single tuple parameter during the init. Remeber that you can always have tuples of tuples. ;)

Kinda OT, but during discussions about that change, was there any talk of representing function arguments as a tuple for each type of arg? Like, "one (possibly empty) tuple of normal args, one (possibly empty) tuple of inout args, one (possibly empty) tuple of @escaping args, etc"? I vaguely recall there was some discussion of making the change, but not anything that was said about why (other than that it'd greatly simplify some stuff in the compiler).

Making tuples be nominal types would unify some things, but I wouldn't take that direction as a foregone conclusion as to where we want to go, since as you note there are also many benefits to keeping tuples "built-in". I think we could achieve the same goals of making tuples more first class, such as allowing them to conform to protocols, without making them into nominal types, letting us have our cake and eat it too.

1 Like

You would still need some way to describe the interleaved sequence of inout, etc. arguments. The many complexities of function types IMO make them also a poor candidate for turning into nominal types, much like tuples.

2 Likes

What cake do we want to eat here? I think nominal tuples are still better as you can provide the same optimizations behind the scene, at least I hope so, and solve more issues regarding tuples and not just conformances to protocols. Here I meant the possibility of explicit disambiguation for the compiler and acceptance of single (labeled) element tuples where it's appropriate to use.

There are two key properties of tuples that I think are important going forward for a variadics design, which I think would be difficult to maintain if they were made into nominal types. First of all, as @technicated noted, there needs to be some primitive type associated with packing and unpacking variadics at the value level. If tuples are themselves a struct, but are also the primitive interface to variadic value, there becomes a circularity issue in defining tuples; alternatively, we'd need to invent different primitives that allow Tuple to be constructed, and I'd be concerned that that design space leads to a much more complex design. Allowing tuples to remain primitive, I believe, allows variadics in turn to be simpler, so I think the complexity cost we pay for special-casing tuples pays off in reducing the net complexity of the language.

Secondly, I think it's very important for the ergonomics of variadics that we keep (T) == T in the type system, and I can only think of very awkward ways we could express that in terms of nominal types. There should not need to be a need to distinguish them in the type system if tuples are integrated with variadics, because we know from type context whether a type variable is variadic or not, and therefore whether a value is concretely a single scalar value or a scalar value bound as the single argument to a variadic parameter.

7 Likes

@Douglas_Gregor
While writing and refining my document on Variadic Generics I wanted to start refactoring the frontend as you said in your first comment on this thread.
So, if I wanted to create this GenericParamDecl wrapper class, what should it extend? Should it be an AST node and extend Decl or something else? I guess that having Decl in the name does not mean that something should be an AST node, right?

1 Like

Just so that nobody misses, I've fired the first PR about this stuff! Let's see what I got wrong :joy:

3 Likes

Just some random thought on the tuple thing...

In a way, we can already do "variadic generic" stuff:

//basic requirement
func foo<S,T>(arg: (S,T)) {...}

//valid calls
foo(arg: ("myString", 42))
foo(arg: (("myString", 42), "1337"))

There are only two problems:

  1. How can we be sure that (A, (B, C)) and ((A, B), C) are treated the same?
  2. The tuples get very hard to read quickly and just look ugly.

Number 1. can actually be solved by just asking the programmer to be careful when doing such a thing. Number 2. is what I understand this discussion is really about. Now, here's a very simple idea how we could ask the compiler to provide the necessary syntactic sugar:

@variadic(S,T)
func foo<S,T>(arg: (S,T)){...}

The variadic annotation would tell the compiler that the tuple (S,T) should be used as base case for (static!) recursion. If someone calls the function with multiple arguments, those arguments would simply be converted to a stacked tuple in whatever direction / order (the programmer implementing foo should not care, that's the whole point). If you want to allow for calls with just one or zero arguments, you would have to reimplement this.

For types with variadic parameters, we could proceed in a similar way, where you would declare

@variadic(S,T)
struct Foo<(S,T)>{...}

As of now, the compiler would unfortunately no-no you if you then tried to create another struct Foo<S>{...} or struct Foo<()>{...} in the same scope. Maybe someone else has an idea how to fix this.

Keeping in mind that the type conversion recursions can happen at compile time and the runtime just ends up with a deeply stacked tuple, the only runtime cost would be that the storage of the function arguments may be noncontiguous due to the nesting (not sure how nested tuples are actually mapped to memory). However, maybe some optimizations to nested tuples may alleviate the problem.

Note that in order to put any constraints on the generic arguments (which kind of is the point), we would need either tuples to be able to conform to protocols or the ability to declare on structs/classes: "I'm the unique tuple type for this protocol that the compiler should use for stacking".

This isn't allowed as the generic parameter is concrete.

Why the need for both type params?:

f(head:Int,tail:Tuple) //without syntactic sugar
f(head:Int,tail:Tuple...) //with syntactic sugar 
//or maybe
f(head:Int,splat tail:Tuple) //with kw splat syntactic sugar