The syntax for variadic generics

I think not having to re-declare generic parameters is a feature of extensions. If generic parameters in extensions don’t show up in code completion, I think we should fix code completion instead of adding more verbosity to extensions.

In any case, my point wasn’t that extensions specifically are a problem, but rather that having a declaration keyword doesn’t really fix the subtlety of using * for pack expansion. Becca’s examples above show how easily * can get lost in an expression.

4 Likes

I really love that we have variadics on the horizon, thank you for pushing this forward. But could you quickly explain two things that I don't quite understand:

  • Why do we need a different keyword on the parameter side of thing, can't we simply use variadic?
  • Why do we need a keyword at all for the parameters, we've already established that T and U are variadic?

For the second question, I am presuming that naked T and U can't be used at all, which is why the repeated keyword seems like it might be superfluous. I mean, these are not well defined, right?

func f<variadic T>(t: T) { }
func f<T>(t: variadic T) { }

func f<variadic T>(t: variadic T) { 
    let s: T = ...
}

But I suppose it's partly about clarity, partly because we wouldn't otherwise be able to say distinguish a variadic tuple (the output) from a tuple of variadics (the input), without introducing boilerplate like a variadic V ... where V = (T, U) or something.

Another thing, more general, is that this particular function seems a bit too magical to me. It seems to be a kind of implicit zip, or can we say that the last repeat is doing the zipping? I think for me there is a general confusion about what the U and T mean on their own, it's not really clear to me what the pack is and what an individual member is. For example, in your syntax, what would these mean, if anything:

By the way, I feel that variadic is a bit misleading since that usually means that the number of arguments is variable, which isn't really the main thing here - for that we could just use arrays. The magic is that the arguments can be different, and hopefully we will also soon have fixed length variadics, which just doesn't sound right....

Anyway, my own syntax of choice is probably curly braces, reminiscent of set notation, for both parameters and generics. It also makes it more concrete I feel, a { T } is simply an ordered set of types, rather than say a sequence of types that varies over time/per call or something. It also gives us a "free brace", so we don't need extra () around complex expressions like { T: Codable }.

func zip<{T}, {U}>(t: {T}, u: {U}) -> { (T, U) } {
  return { (t, u) }
}

I'm not sure I think that already having a reserved word repeat is a strong argument, in the long run isn't it more important that the syntax is clear than saving one reserved word? Especially since it means a different thing currently.

2 Likes

I really like the overall idea of many and each. I disagree on some small details.

I’m not sure if pack properties are in the scope of this discussion, but I think they shouldn’t be allowed. A tulle would be much more straightforward in this case.

I’m not sure I understand your argument here. Since type packs are abstract entities and cannot (or at least I don’t foresee they can) conform to a protocol/class as a pack, it’s quite unambiguous that the conformance constraints refer to individual pack elements. Also <T: many>, if I understand your proposed syntax correctly, looks pretty awkward. The only advantage I can think of is that if we were to adopt labels for type parameters, the keyword after the colon would simplify the syntax.

My only reservation with this is that many & each mean different things entirely from some & any yet they are all quantitative words. I wonder what the impact of this will be on readability when used together, especially for first time learners. It may become quite ambiguous.

5 Likes

Or combined… many some Foo?

3 Likes

Is there any conceivable/reasonable way that double angle brackets could be used?

<<T>>

What would that look like in practice? Functor<Result, <Args>>?

I wonder if surrounding | could serve us here for both variadics and packs:

struct VariadicZip<|Collections|: Collection>: Collection {
    var underlying: |Collections|
    // ...

... along with the map syntax proposed by @beccadax.

Not sure if there's any prior art for the | character in Swift (except || for logical "or" of course).

Swift also uses | for the bitwise operator as in C, and I’ve seen custom operators like |> etc.

I understand the value of this, but I worry about using too-general terminology for what is ultimately an advanced feature. I definitely don't think the keyword would need to literally say pack, but I also don't think programmers using variadic generics will get away with not encountering the pack/expand terminology.

The other thing to keep in mind is adding a new keyword in expression context is a source breaking change. I got away with any as a contextual keyword because you don't need to parenthesize the operand, so any (x) still parses as a function call (and in fact there is a free any function in the standard library). We can't get away with that for variadic generics, because you need to be able to parenthesize the argument.

The way I think about it is the keyword is a property of the type parameter itself, not any conformances that apply to pack elements. I think <many C: Collection>, and <variadic C: Collection> both read pretty naturally as "a variable number of type parameters that conform to Collection. I will also say that one benefit of using a prefix operator/keyword is that conformances can be naturally written in angle brackets, whereas <C...: Collection> doesn't feel quite right to me, because the conformance applies to the pattern type and not the expansion itself.

I disagree that this is clearer than the keyword or operator equivalent. I actually find this syntax pretty misleading for a few reasons:

  • .map isn't only used for mapping elements to a new element, it's also used for direct forwarding. For example, you cannot write
func tuplify<variadic T>(_ t: each T) -> (each T) {
  return (t) 
}

References to packs must appear inside pack expansions, so you would instead have to write

func tuplify<variadic T>(_ t: each T) -> (each T) {
  return (t.map { $0 })
}

which is very different to the way .map is used today. An old design exploration for variadic generics used a map-style builtin, but allowed exact forwarding to omit the .map { $0 }. I think that privileging exact forwarding would be pretty frustrating, because you would need to add .map { $0 } as soon as you want to append other elements to either side of the pack expansion, and it wouldn't work for other values that you might want to turn into packs such as tuples or arrays. For those, you would always need to write tuple.element.map { $0 } or array.element.map { $0 }.

  • Expanding two packs in parallel does not need to iterate over the packs twice, but using zip(t, u).map { ... } looks that way.

  • zip and map are not real functions. They do not return a single value; they compute a comma-separated list of values. Further, zip and map would still need to be resolved via overload resolution amongst the existing overloads, so this approach doesn't help much with the type checking issues that ... has.

  • We have to reconcile the fact that we need to express the same list operations at the type level. I think the programming model is so much simpler if you express the same operations in the same way at the type and the value level. I'm open to being convinced that a type-level zip and map are the best approach, but I really don't think this helps anybody:

func zip<variadic T, variadic U>(t: T.map { $0 }, u: U.map { $0 }) -> (zip(T, U).map { ($0, $1) }) {
  return (zip(t, u).map { ($0, $1) })
}

I also don't want to mislead people into thinking I have any interest in pursuing type-level filter builtin :slightly_smiling_face:


I think that each or repeat are closer to the right mental model for pack expansions. Consider the signature of List.firstRemoved from the vision document, adapted to use your many/each syntax:

struct List<many Element> {
  func firstRemoved<First, many Next>() -> List<each Next> 
      where (each Element) == (First, each Next)
}

This is how I read the declarations:

  • The struct List has a variable number of type parameters called Element
  • The firstRemoved method is parameterized on a List type parameter and a variable number of Next type parameters. It returns a new list of each Next type, and it requires a tuple of each Element to equal a tuple of the First type followed by each Next type.

I do see a problem with the word "each" though; "each" is typically used to refer to individual people or things separately. So List<each Element> could easily be misinterpreted as "each Element is in its own List". Hmm...

The way that I've been describing patterned pack expansion is along the lines of "given a concrete pack substitution, the pattern is repeated for each element in the substituted pack." If we want something more verbose that more directly spells this out, we might consider something like...

extension List<many Element> {
  func firstRemoved<First, many Next>() -> List<repeat each Next> 
      where (repeat each Element) == (First, repeat each Next)

  func split() -> (repeat List<each Element>)
}

In the above example, I'm using many as a declaration introducer, repeat applied to repetition patterns, and each in front of pack references to refer to elements of the pack individually.

This is super verbose and it's a lot of keywords. But, each can be a contextual keyword, and it addresses some earlier concerns that given a pack expansion, it's not syntactically obvious which references inside the pattern are packs versus scalar values. This is actually the one thing about the zip approach that I like; it pulls the pack references to the front of the expression before writing out the pattern, so it's clear upfront which things are packs.

I think syntactically distinguishing pack references in repetition patterns would be particularly helpful for accessing tuple elements as a pack, and it offers a disambiguation strategy for the property pack called element and a label called element. Here's an example of tuple.element using each:

// Pretending 'Tuple' is a typelias to the horrendous <many Element> (repeat each Element)
extension Tuple: Equatable where each Element: Equatable {
   public static func ==(lhs: Self, rhs: Self) -> Bool {
    for (left, right) in repeat (each lhs.element, each rhs.element) {
      guard left == right else { return false }
    }
    return true
  }
}

And selfishly, I certainly would like it if the compiler could identify pack references syntactically, because opening pack element types in the constraint system is a little tricky :slightly_smiling_face:

I'm not necessarily saying that any of this is a good idea. It's definitely some hot keyword soup. These are just some thoughts I arrived on while thinking through all of your excellent points!

5 Likes

It's a pity that the ... syntax isn't practical, but I like the many / each syntax suggested by @beccadax as a good alternative. It fits well with existing keywords some & any. I might even grow to like more than the ...'s with time.

1 Like

It doesn't need to be a different keyword between declaration and expansion. We could pick one keyword and use it for both declaration and pack expansion, similar to how ... is used for both in the current experimental implementation.

An explicit operator or keyword is needed for pack expansion because there are semantic differences depending on where the operator is written. For example, <variadic T> repeat List<T> and <variadic T> List<repeat T> mean two different things:

  • <variadic T> repeat List<T> says to repeat the entire pattern List<T> for each element in the type parameter pack T individually. Substituting T := Int, String, Bool produces List<Int>, List<String>, List<Bool>.
  • <variadic T> List<repeat T> expresses one single type List with a generic argument list containing each element in the type parameter pack T. Substituting T := Int, String, Bool produces List<Int, String, Bool>

Yes, repeat is doing the zipping by way of repeating the pattern N times for N elements in the pack.

Because T and U can only be written inside of a pack expansion, they always represent an individual pack element. A conformance requirement T: Collection says "each element in the pack conforms to Collection".

variadic in this context really means that the number of type arguments is variable.

This suggestion has come up a few times for referring to packs. Curly braces doesn't really work because we need to use the same notation in expressions, where curly braces already mean something. For example:

func expandClosure<variadic T>(t: repeat T) -> (repeat () -> T) {
  return (repeat { t })
}

The above function returns a tuple value whose elements are closures that return each element of the value pack t, individually.

The reason why I thought to use repeat is because I think that's the best word to describe what's happening. In the current proposal, pack expansion is expressed using repetition patterns. You write a pattern involving pack references, and the pattern is repeated for each pack element in the substitution at runtime. I agree that it's unfortunate to introduce another meaning for repeat, but IMO it might be better than introducing another meaning for .... If we also use a contextual keyword for pack references, pack expansions will look quite different from repeat-while loops.

1 Like

I'll note that I think this is similarly tricky for humans, in addition to the constraint system. Wherever we land with the syntax on this, I would really like it if it were relatively easy to locally identify variadic pack types and pack references. This is one of the things I've harped on with the ... syntax with respect to its use in type signatures (since at the point of use, you may not be able to tell if Element... is a (normal) variadic parameter of say, some nested Element type, or a variadic type pack expansion of the generic Element type. Perhaps this will often be clear from context, but it's still an additional speed bump that makes it difficult to quickly scan and understand code.

A similar thing applies to pack references, as you've noted. Since expansion patterns may be arbitrarily complex (modulo what the constraint system is able to handle), marking only the bounds of the expansion with ... is insufficient to understand what is getting expanded. Again, maybe you can figure this out by closely reading each reference in the subexpression, but that's a closer reading than I feel should really be required to quickly glean "what's getting expanded, and into what?"

In the past I've suggested the explicit pack syntax that we've used to discuss the feature informally, that is, enclosing the pack in braces. That would give us for the for loop:

    for (left, right) in ({lhs.element}, {rhs.element})... {
      guard left == right else { return false }
    }

I'm not sure that braces is the right thing here (and obviously has the downside of being visually confusable with a close expression containing a single pack reference), but I would really support having something to mark pack references explicitly, so wanted to +1 this point in particular.

1 Like

I can see what you mean with these points.

Thinking about it more, I think what I like most about the map-style syntax is that the extent of the code it covers is immediately obvious, whereas with repeat you have to figure out where the enclosing expression ends. This is okay for effect markers like try that can cover much more of the expression than they need to without any change in behavior, or for introducers like any which bind very closely to a fairly constrained piece of syntax, but repeat can appear at many different positions and exactly what it encloses matters quite a bit. map doesn’t have that problem.

So let’s invent a strawman syntax—I’ll use #{<expr with packs to expand>}—that explicitly encloses its subexpression like map does but doesn’t look like a method call, and let’s also adopt it at the type level so that expressions and types echo each other:

printPack(tuple, #{pack})               // Concatenating tuple with pack
#{printPack(tuple, pack)}               // Expanding tuple with pack
printPack(#{tuple.element}, #{pack})    // Concatenating tuple element pack with pack
#{printPack(tuple.element, pack)}       // Expanding tuple element pack with pack

struct List<many Element> {
  func firstRemoved<First, many Next>() -> List<#{Next}> 
      where (#{Element}) == (First, #{Next})

  func split() -> (#{List<Element>})
}

struct VariadicZip<many Collections: Collection>: Collection {
    var underlying: (#{Collections})

    typealias Index = (#{Collections.Index})
    typealias Element = (#{Collections.Element})

    subscript(i: Index) -> Element { (#{ underlying[i.element] }) }

    var startIndex: Index { (#{underlying.startIndex}) }
    var endIndex: Index { (#{underlying.endIndex}) }

    func formIndex(after index: inout Index) {
        // This feels a little funny:
        for (c, inout i) in #{(underlying, index.element)} {
            c.formIndex(after: &i)
        }
    }
}

I don’t know about you, but I like this general feeling. It’s more structured than “keyword soup” but doesn’t look like a normal expression syntax, either.

(I’ve removed each from these examples, but in principle there’s no reason it couldn’t be reintroduced.)

1 Like

I think there's a design that at least resolves the formal ambiguity with the use of curly braces. E.g. if pack references in expansion patterns are required to be wrapped in curly braces then what you've written above actually just expands the pack elements into the tuple (and not first into a closure). The version of this function that type checks would be:

func expandClosure<variadic T>(t: repeat T) -> (repeat () -> T) {
  return (repeat { {t} })
}

which I recognize is... not ideal.

But... since something like {lhs.element} could be a valid expression depending on how the element name lookup turns out I guess this wouldn't solve the issue of needing to open pack references in the constraint system anyway. Do we really have any syntactic room left for a concise "pack reference" sigil that wouldn't run afoul of (potential) operator overloads, or would we need to fall back to something like a fully-spelled-out each in order to escape the "opening in the constraint system" issue?

I agree this feels like a pretty important goal and +1 to it being a major benefit of the map-style syntax. At the same time I share many of Holly's concerns about making this look like a 'normal' Swift expression/function call...


A question that just occurred to me:

What's the expectation of what one should do with this for-in statement if the expansion pattern started getting long and complex? We can't just pull the expansion out into a local variable, can we? Or would the move here to be expand into a tuple-typed local variable and and then expand the tuple into a pack and expand the pack into the for-in?

A shorter keyword for the expansion of a variadic pack which seems to be possibly valid in many if not all scenarios could be emit.

func expandClosure<pack T>(t: emit T) -> (emit () -> T) { ... } 

A concern I have with the strawman syntax is that # implies “compile-time”, but expansion of variadic generics happens at runtime. #{pack} might further mislead programmers coming from C++ into equating generics to templates, especially if expression macros become available around the same time.