Starting to implement Variadic Generics

I'm sorry, I don't understand what you mean here - not saying this in a sarcastic way, I literally do not understand what you mean by "tolerance for design discussion".

Maybe I'm being paranoid, but I was only worried that you had come in here asking for implementation guidance, and then we all started arguing about aspects of the design. I could see some people being put off by that.

1 Like

Ah, now I understand!
I'm personally not too concerned about this, but obviously for reference and discussion simplicity I'd like to keep the two aspects separated because like you say not everyone might be happy with things being mixed.

Oh yes, very good point. Does this mean that variadic parameter packs will be "what tuples should have been"? Variadic parameter packs don't need to be varargs (in the current sense) so that is one bit of complexity they wouldn't have to eat. I think the only other things that are missing are the ownership modifiers and autoclosures.

Does ownership give us a new way to introduce inout elements to tuples? What will the fundamental difference be between a parameter pack and a tuple? We don't have partial specialization, so we can't use the C++ model for variadic templates, we need some sort of "dynamic" model.

-Chris

Random thoughts about representations:

  • I don't know if the runtime representation of a variadic pack of types should be an ArrayRef or a pointer to "Pascal-style" array or what, but I know it should not be flattened into the surrounding types.
  • That's when storing the pack in type metadata; as an argument to a function I think an ArrayRef is the way to go, especially if recursion is going to be important somehow.
  • If we want to do piecemeal destructuring of the value representation, and we use a tuple for the representation of a variadic pack of values, then we should promote pulling elements off the end rather than the beginning or else we'll have to move the elements in order to make the padding right.
  • That said, it might be more efficient to use an array of pointers to values.

@Douglas_Gregor @Chris_Lattner3
I was writing my document about Variadic Generics when I stumbled across Chris's post on the Introduce (static) callables thread. In the post, Chris mentions a long term goal of unifying nominal and structural types. If I'm correct, this desire is shared among multiple Swift users and other thread refers to this feature, too.

At the moment I'm thinking to represent Variadic Generics as tuples inside types and function bodies, much like variadic function arguments are passed as Arrays.
But if / when we have structural types for tuples and / or functions, because they will need Variadic Generics (I assume), they might look like this:

struct Tuple<T...> {
  // Tuple implementation
}

class Function<Args..., Result> {
  // Function implementation
}

But at this point it seems to me that we have a chicken-and-egg problem: Tuple definition requires Variadic Generics, but Variadic Generics are represented as tuples inside the type using them, so...

struct Tuple<T...> {
  // This is... a tuple?
  private var members: (T...)

  init(_ members: (T...)) {
    self.members = members
  }
}

So, looking at the (far) future, what can we do here? The only solution I can think of ATM is to actually not represent Variadic Generics as tuples, but as something else like a "parameter vector" or C++'s "parameter pack".

3 Likes

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