SE-0393: Value and Type Parameter Packs

This proposal uses each in several places; in which positions/circumstances would you expect it to be inferred?

@Jumhyn I'd say it can be inferred in all, since the compiler knows it's dealing with type fold/packs from the signature/definition. Would you not agree / have a certain edge case in mind? I might be missing something since I've just quickly read the proposal, but seems like a sane assumption.

Inferring each would have significant performance overhead in the type inference system, it would make future evolution prone to ambiguities, and it would degrade the experience of other tools such as code completion. In this initial proposal, each can only be applied directly to value pack references because value packs can only be declared in function parameter lists. In the future if/when value packs are allowed in other places, such as stored properties or converting tuples containing pack expansions to value packs, it is not immediately obvious -- to a human or to the compiler -- which values inside an expression are packs, and whether or not an expression is a value pack can differ e.g. across different overload choices, introducing new ambiguities in variadic generic code. I strongly believe that explicit each is the right programming model.

4 Likes

Got it and makes sense, but this is an implementation detail and I'm in no position to have an opinion about it. As an user, more working inference would be better. Obviously, I'd rather write each than having slow autocompletion or "compiler is unable to type-check this expression in reasonable time" errors

Not sure I follow this. There would always be a keyword to mark a pack:

let stored: pack T
let stored: fold T

The syntax for these function declarations looks great. I like the use of repeat and each rather than symbols / sigils.

I'm trying to understand what sort of functionality you can actually implement in the function body of these declarations, using the features included in this initial proposal. For example, with the < example in the motivation:

func < <each Element: Comparable>(lhs: (repeat each Element), rhs: (repeat each Element)) -> Bool

How would we implement the body of that function? Is that possible using just the features in this proposal, or would it require implementing some of the future directions like pack iteration / pack indexing? If so, are those in the works as follow-up proposals?

1 Like

I'm sympathetic to this point, but I think the reasons for choosing each are stronger than repeat. I chose each for type parameter pack declarations because:

  • I wanted to limit the choices to each or repeat to avoid introducing a third keyword in variadic generic code.
  • Using each means everywhere you write a reference to a type parameter pack, you always write the same keyword.
  • Conformances can be written in generic parameter lists. Conformance requirements always apply to elements of the pack, and elements of the pack are written as each T.

My experience writing code under this feature for the last few weeks has been that I'm not bothered by the fact that func zip<each S> or struct ZipSequence<each S> read a little funny, and the consistency of always writing each on pack references is helpful.

each is conceptually very different from some. The way to think about each T is that each T is an individual element of a parameter pack. Code that operates on parameter packs is always written inside of a repetition pattern, where the pattern is expressed in terms of an individual pack element. At runtime, that pattern is repeated for each element in the substituted pack.

For example:

func printEach<each T>(_ t: repeat each T) {
  repeat print(each t)
}

printEach(1, "hello", true) 

The above printEach function will repeat the call to print for each value in the value parameter pack. The output of the function call is:

1
hello
true

So, for the folks saying that repeat reads like a loop

Exactly! The syntax for repetition patterns should evoke iteration, because that's exactly the runtime execution model for parameter packs in Swift. Once you internalize the idea that repetition patterns are fancy loops that can handle distinct types across each iteration, you'll realize imperative code that abstracts over arity can be expressed with parameter packs, too.

The "expansion" term really only describes the result of evaluating a repetition pattern under substitution, i.e. a single pattern turning into a comma-separated list of things. I haven't used variadic generics in other languages, and I really didn't have a solid mental model of "pack expansion" until I had a much deeper understanding of parameter packs through writing out the detailed design of this proposal. I personally find that the "repetition pattern" terminology encourages a much more natural mental model of how the code actually behaves, and I wonder if we should steer away from "expansion" and toward "repetition" in documentation, diagnostics, etc.

7 Likes

I believe the only future direction required to subsume the tuple operator overloads with parameter packs is using tuple elements in repetition patterns. There's an experimental implementation of this on main using the each tuple.element syntax, and @Sophia and I are working on a pitch. This builds and runs on main under -enable-experimental-feature VariadicGenerics:

func isEqual<T: Equatable>(_ lhs: T, _ rhs: T) -> Bool {
  lhs == rhs
}

func == <each Element: Equatable>(
  lhs: (repeat each Element),
  rhs: (repeat each Element)
) -> Bool {
  var result = true
  repeat (isEqual(each lhs.element, each rhs.element) ? () : (result = false))
  return result
}

print((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) == (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))
print((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) == (1, 2, 3, 4, 5, 6, 100, 8, 9, 10, 11))

The output is

true
false

There are other future directions that would make the implementation a little nicer, but it's expressible!

4 Likes

Set requires its element type to be Hashable, so the generic signature of foo() is really <C where C: BidirectionalCollection, C.Element: Hashable>. If you spell it out directly you'd get the same error since tuples cannot conform to Hashable (or anything else) right now.

fold in functional programming means something different (it's like reduce() in Swift). The iteration performed by repeat is not a fold because it's not threading an accumulator through from one iteration to the next.

4 Likes

Iā€™d expect the requirement C.Element: Hashable to be explicitly spelled out ā€” so if it isnā€™t now, implicit same-shape inference makes sense.

1 Like

Right. It doesn't have to be spelled out today, it's inferred from the application of Set<> to C.Element. I know this feature is somewhat controversial and occasionally someone posts on the forums because they're surprised to see it happen. A pitch to deprecate this behavior in Swift 6 could probably get some traction, but we would definitely need an explicit spelling for same-shape requirements in that case.

4 Likes

Iā€™d like to add some comments for the each (T, U) syntax:

I believe the syntax is self-explanatory and intuitive for Swift developers, but I do have concern about it because weā€™re reusing each in pack expansion, and in that case (T, U) could be a real tuple type, which we require to be spelled out as repeat (each T, each U) now. This is somehow confusing because theyā€™re different things that happen to have similar (or same with a syntactic sugar) syntax.

However, I see thereā€™re heated discussions over the repeat each syntax and we may end up without each (or whatever keyword we use for type parameter packs) in pack expansions. In such cases, I believe each (T, U) as same-shape requirement syntax should be added directly in this proposal because itā€™s a clear and tiny change that really adds up to expressivity.

@Slava_Pestov It is a bit (conceptually) like reducing or folding multiple type parameters into one - a type parameter pack.

You have now given two examples where you used a pack expansion expression as if it were a statement. However, the proposal clearly states that

Would your examples work like you wrote them? Or would they just work if the pack expansion exressions were surrounded by parentheses?

Wait, does this proposal support using closures with parameter packs in the capture list? Or are you just talking about future growth in this area?

Yes, I built and ran both of these examples with the current implementation before posting. I'll clarify in the proposal that "position that naturally accepts a list of expressions" includes contexts that accept a comma-separated list of expressions as well as top-level expressions.

I understand there's an implicit same-shape requirement. What happens if I try to violate that requirement by trying to pass packs of two different arity?

Using Holly's == example, what does the compiler tell me if I attempt to do this?

let outOfShape = ((1,2) == (1,2,3))

Well, I guess it likely just tells me that it can't find a function in scope named == that takes those two arguments, like if I tried to use == on two unrelated types, but is there a way to make it smarter about it and be specific about the same-shape requirement in the error?

Yes, you can capture pack elements in closure patterns with this proposal.

Same-shape requirements only apply when there are multiple parameter packs. My example has only one parameter pack each Element, with two arguments of the same tuple type, so any arity mismatches will manifest as a tuple type mismatch, e.g. cannot convert value of type '(Int, Int, Int)' to expected argument type '(Int, Int)', etc. You're right that in the presence of overloads, the ambiguity message will say something to the effect of no overloads existing that take the given argument types.

Here's an example of code that produces a same-shape requirement error:

func zipLocally<each First, each Second>(
  first: repeat each First, 
  second: repeat each Second
) {
  repeat (each first, each second) // error: pack expansion requires that 'Second' and 'First' have the same shape
}

// same-shape requirement inferred from parallel repetition of 'First' and 'Second' in the return type
func zip<each First, each Second>(
  first: repeat each First, 
  second: repeat each Second
) -> (repeat (each First, each Second)) {
  return (repeat (each first, each second)) // okay
}
3 Likes

I donā€™t know if this has been mentioned before, but what if we were to constrain the generic type to a (new) variadic type, instead of marking it as a variadic generic with the each keyword?

Basically, instead of writing

func foo<each T>(_: repeat each T) -> each T {}
struct Foo<each T>  {
    let foo: (repeat each T)
}

Would it be possible to write

func foo<T: pack>(_: T) -> T {} // we know T is a pack
struct Foo<T: pack> {
    let foo: T.each // equivalent to (repeat each T)
}

If we want to constrain T to be a pack of Sequences, we could do

func foo<T: pack & Sequence>(_: T) -> T {}
struct Foo<T: pack & Sequence> {
    let foo: T.elements // equivalent to (repeat each T)
}

// also possible
func foo<T>(_: T) -> T where T: pack, T: Sequence {}
struct Foo<T> where T: pack, T: Sequence {
    let foo: T.elements // equivalent to (repeat each T)
}

By pack being a type rather than a keyword, that could also give devs the flexibility of extending its functionality

type pack { 
    // needs some kind of element semantics

    var elements: (Self.Elementā€¦) // equivalent to (repeat each T)
    var count: Int // number of elements in a pack
    func map<T>(_ transform: (Self.Element) throws -> T) rethrows -> [T] // iterates over each element of the pack and transforms it
}

EDIT:
So thinking about this a little longer, instead of constraining T to pack & Sequence & OtherProtocol, we could parametrize the pack constraint
e.g

type Variadic<Element> {} // or type Pack<Element>

func foo<T: Variadic>(_: T) -> T {}
struct Foo<T: Variadic> {}

func baz<T: Variadic<Sequence>>(_: T) -> T {}
struct Baz<T: Variadic<Sequence>> {}

Thank you!

I think this is a fantastic proposal, and the right first step toward variadic generics. The care and level of detail provided in this proposal are exceptional, and I feel like it's hit the design sweet spot for such a challenging feature.

I have some quibbles with a few details of the proposal, which I'll get to below, but they are all minor.

It is. I included variadic generics in the "Generics Manifesto" years ago, back before we had a process for vision documents, because it's important kind of abstraction we're missing from the language. Folks end up writing lots of overloads based on arity, stamping them out manually or with external-source-generating tools, which is quite unfortunate. Macros will make some of this easier, but it still won't be a good state.

Yes; we've been trending toward this for a while. The syntax proposed here---with repeat and each fits well with the way we've been introducing similar type-system features (some, any, borrowing, etc.).

I'm the original designer and implementer of C++ variadic templates, which are the most similar language feature to this. The design proposed here is better and more comprehensive than the direct "port" of variadic templates into Swift that I had previously been thinking of.

Read through the proposal and earlier iterations, light involvement in pitches, and watched the implementation from the sidelines.

Now to my actual commentary...

Elision of repeat for generic requirements

Early in the proposed solution, there is this exemption from the need to write repeat for generic requirements:

A pack expansion consists of the repeat keyword followed by a type or an expression. The type or expression that repeat is applied to is called the repetition pattern. The repetition pattern must contain pack references. Similarly, pack references can only appear inside repetition patterns and generic requirements:

func zip<each S>(_ sequence: repeat each S) where each S: > Sequence

I don't see any reason why this should be the one place where we allow the elision of repeat. It's inconsistent, and it feels like it's going to trigger ambiguities when there are local generic functions:

func f<each T>(_) {
  g(repeat {
    func h<U>(_: U) where each T == U
    })
}

For the nested functions inside that closure, the proposal means that all of the Ts == U, because there's an implicit repeat for the generic requirement, but perhaps I meant that only the current T is equal to U. I can't express that with the proposal now, but I think I should be able to. Making the repeat explicit for generic requirements, like it is everywhere else, improves expressiveness and eliminates an unnecessary special case.

There is a small writing thing in the proposed solution where we introduce pack expansions that tripped me up

The syntax for a pack expansion type is repeat each P , where P is a type containing one or more type parameter packs.
Written as repeat each expr , where expr is an expression referencing one or more value parameter packs.

In both cases, the each isn't part of the syntax of pack expansions: the syntax is repeat P or repeat expr, respectively, and there's a semantic constraint that somewhere in the type or expression (or generic requirement, per my comment above) that there be at least one each in there that isn't covered by another repeat.

each and parentheses

We will refer to each T as the root type parameter pack of the member type parameter packs (each T).A and (each T).A.B .

The required parentheses here are annoying. I feel like we've been through this before with (any P)? and regretted being pedantic. Could these rules be relaxed in some way, so that each T.A and each T.A.B would be acceptable?

If we get this simplification, we'll have to decide what it means if some day we get associated type parameter packs, where any of T, A, or B could be packs. I'd be fine with .each X indicating a nested parameter pack that's getting expanded, e.g., T.each A.B for cases where B is the pack that's being expanded.

Value parameter packs

Minor nit, but this one-off sentence before the "capture" discussion

Note that pack expansion expressions can also reference type pack parameters, as metatypes.

reads as if the only use of a type pack parameter in a pack expansion expression is as a meta type; that not true, since there are other syntactic constructs one could use such as as? each T that aren't metatypes. I suggest either generalizing this sentence or striking it.

Overload resolution

There are other replies about the overload-resolution behavior, so we need not belabor the point here, but I find the fact that this is to be ambiguous:

func overload<T>(_: T) {}
func overload<each T>(_: repeat each T) {}

overload(1)

to be surprising. Usually, I've tried to describe the overloading rules as "if the parameters of A can be forwarded to B, but the converse is not true, A is more specialized than B". That hints that the fixed-arity version is more specialized than the variadic version, which also makes intuitive sense. (It's the same, but for just one argument)

Macros!

Value parameter packs are defined thus:

  • A value parameter pack is a function parameter or local variable declared with a pack expansion type.

but we also want macro parameters to be eligible to be value parameter packs.

Future directions

I'm very sad to see pack iteration subsetted out of this proposal. I understand the complexities here, but this feels like one of those places where we can take a very difficult feature and make it accessible.

Doug

18 Likes