Pitching The Start of Variadic Generics

Plus all the 1s!!!

I just got to work so I’ll have to actually read it later, I just saw the title and couldn’t contain my excitement is all.

:grin::grin::grin:

I think @xwu is right about the splatting operator. There's no reason we can't use a prefix * for it. I'm not sure about the spelling for type sequences, but I can't really think of anything better than either T... or ...T off the top of my head.

Everything else seems great, especially with parameterized extensions.

I'm super excited about this feature, so thanks so much for championing it!

This pitch makes sense to me overall, but one aspect confused me: In the allPresent example, I didn't expect the where T == Set<U> clause to map the constraint element-wise. I'm not sure what it would do otherwise though, and more explicit syntax is probably needlessly verbose, so maybe this is fine.

1 Like

In terms of the spelling for a type sequence, if using a prefix * for the splat operation, might it not make sense to use * on the other side, too? So instead of T... use T*. Then you could spell that potentially undesirable matrix example as func matrix<T*>(_ xss: (T*)...) if I'm understanding it correctly.

I don't know if that's really better than the dots or not, but... maybe?

14 Likes

I like this a lot, though I suspect that the details will evolve significantly over time (especially, I think, for the reasons/objections that @Jumhyn laid out).

A couple of comments specifically about the pitch, though:

— The allPresent2<T...> example is unnecessarily confusing, I think, because it uses T where the allPresent example used U. It seems like a trap for the unwary.

— The statement "This means we have the ability to construct generic functions that don’t just take type sequences, but return them as well." is followed by an example that doesn't actually return a T..., but a tuple (T...) that wraps the T....

The document doesn't actually show whether ( … ) -> T... is possible, it just says it is.

So, I think there is a different discussion to be had here, about storing and (perhaps) composing T... values. It's also interesting to consider whether there might be something analogous for non-generic variadics, too, so that we don't have to turn cartwheels to bridge between (say) Int... and [Int].

1 Like

I really want to see variadic generics in the Swift, so I'm very happy to see this pitch.


Composition of type sequences would be hard with this pitch. For example, I can imagine a type witch takes variadic (T) -> U arguments. But maybe it is difficult:

// desired behavior
foo(closures: { (a: Int) in a + 1 }, { (a: String) in print(a) }, { (a: Bool) in a ? "true" : "false" })

// maybe error, since the length of T... and U... is not always equal
func foo<T..., U...>(closures: ((T) -> U)...) {}

// however, since there is no relation ship between T... and U..., constraint like `T == Set<U>` cannot be added

It's just an idea, but maybe some (T, U)... type sequences can solve this problem.

// maybe error, since the length of T... and U... is not always equal
func foo<(T, U)...>(closures: ((T) -> U)...) {}

With it, allPresent example changes like this:

// it ensures that T... and U... have the same length
func allPresent<(T, U)...>(haystacks: T..., needles: U...) -> Bool
  where T == Set<U>  // such constraint can only be added to (T, U)... type sequence
{ /* ... */ }

Does T... allows zero-length type sequence? Considering current variadics allows zero length parameters, it is natural to allow them.

func foo<T...>(_ value: T...) {}
foo()

However, I think then it is difficult to express some constraints. Consider creating HomogeneousTuple struct.

struct HomogeneousTuple<T...> {}

Of course this is not correct, since T... allows heterogeneous types. Therefore, you have to write like this:

@dynamicMemberLookup
struct HomogeneousTuple<Element, T...> where T = Element {
    var tuple: (T...)
    subscript<U>(dynamicMember keyPath: WritableKeyPath<(T...), U> -> U {
        get { tuple[keyPath: keyPath] }
        set { tuple[keyPath: keyPath] = newValue }
    }
}

However, on the use-site, it becomes strange type:

let hTuple: HomogeneousTuple<Int, Int, Int> // this is two-length HomogeneousTuple actually!!

Maybe some solution must be prepared for it. I thought generic where I described here can be one solution, but there will be neat way.


Some thought:

  • Is there any possibility to consider variadic - reverse generics? Returning opaque length of type sequence would be intresting.

  • I don't think the strategy to have _countTypeSequence, _mapTypeSequence, and other build-in operations is enough. Maybe it turns endless addition of reserved name for zip, reversed, rotated, and so on? But I don't have any idea to enable such operation without built-in :cry:

    • In the allPresent example, zip(haystacks, needles) naturally appears, and I'm confused. Do type sequences conform Sequence?

+1. Interesting to hear if tuple fusion can be possible using this technique (like TypeScript's variadic tuple types).

// NOTE: This signature is probably wrong...
func fuseTuple<T...>(_ xs: (T...)...) -> (T...) { return ??? }

fuseTuple( ("Hi", 42, 3.14), ("foo", true) ) == ("Hi", 42, 3.14, "foo", true)
2 Likes

Compound sequences would be nice, yes.

func concat<T…, U…>(_ ts: (T…),  _ us: (U…)) -> (T…, U…) { /**/ }

Unfortunately a little too advanced for the first go.

3 Likes

Personally I wouldn’t mind the variadic T spelling instead of T..., it’s not shorter but much pleasant to parse as a human.

17 Likes

I like the keyword idea. Also something like many T could be interesting.

11 Likes

This will be great to have!

Thinking on ways to address some of the challenges, a thought came up—what if we address the current limitation by allowing arguments (variadic or not!) to have opaque generic types?

func print<Item>(_ items: some Item...)
  where Item: CustomStringConvertible {
  
}

The trick, of course, would be the type of items: [some Item]—an array with an opaque element type, which is currently disallowed. On the plus side, it does (imho) feel like a more direct semantic interpretation of heterogeneous variadics.

I’m aware this is quite different from @codafi’s pitch (and would warrant a separate proposal), but I think many of the described type sequence affordances translate moderately well to arrays of opaque types (with a couple of exceptions I can immediately see).

There are many aspects, implications, and variants to explore, but being quite new to this type of discussion, I wanted to see whether someone can correct me if it’s obviously problematic, or actually worth diving in deeper on and writing up? :blush:

I really like the idea and where this is going, thanks for writing this :+1:

I agree that ... syntax for type sequences might be confusing and ambiguous. I for one got a bit confused as I was reading some of the examples as if they were declaration for variadic arguments instead of type sequences sometimes, and I feel it'd be too easy to confuse them, while it's important to make clear that the different types in a T... type sequence are not necessary the same types each as opposed to variadic arguments.

I also found it a bit confusing to see typealias Element = (Sequences.Element...) in the zip example, as I was expecting the syntax to be more something like (Sequences...).Element or needing some usage of _mapTypeSequence here, as were effectively wanting the element of each archetype here.


Some alternatives I have in mind as an alternative to ... and to avoid ambiguity:

  • postfix * — ie T*. Would also maybe allow things like typealias Element = Sequences*.Element
  • postfix [] — ie T[] and typealias Element = Sequences[].Element

Personal opinion on those:

  • I like * because it's common and would work well in conjunction to introducing a potential splat operator too using * too (a bit like in Ruby).
  • I like [] because when I see func debugPrint<T[]>(_ x: T[]) I think of the type sequence as being "a tuple (T0, T1, T2,… Tn)" or (T[0], T[1], … T[n]) — as if those were mathematical subscripted variables, which acts as a reminder that each Tn can be different (as opposed to T... used as a variadic generic where they'd be all the same).
  • It would also open the door in the future to allow specifying an arity or even a range, eg T[5] for a type sequence / tuple with 5 (possibly different) types, T[1...] for a type sequence of size at least 1, etc
  • But otoh for some subjective reason I feel like (_ x: T[]) is less suggestive of the method allowing a variable number of (potentially heterogeneous) arguments and instead suggests an arity of one there, while I somehow feel like T* would make it more natural to read it as variadic arity (maybe that's my Ruby bias speaking here?)

Could we have some clarification on (or removal of) the word “archetype”? As far as I can tell it’s implementation-detail jargon. Slava was going to explain it in part four of his two-part series back in 2016…

7 Likes

I think this is wonderful, but I wonder if the use case that I'm personally most interested in is possible to express, namely homogeneous tuples of variadic length? It's very useful in math related applications, especially of course with vectors. Something like:

struct Vector<S> {
    let value: S...
}

Also, I imagine other will be interested in the opposite case, heterogeneous tuples of a given length. Yeah, that's just tuples, but with the proposed syntax you don't have to explicitly name each type.

struct Quadruple<S[4]> {
    let value: S[4]
}

I don't mind the syntax, I'm sure it will be fine, but I suppose we should first make existing generics accept this shorthand (which is very useful):

extension [String] { } // = "Array where Element == String"

By the way, I didn't really understand what _forwardTypeSequence does, could you perhaps add another example or a few words of explanation?

1 Like

many T suggest that they are all the same, which is an important use case, but perhaps it would be less clear for the general case.

I also like the "zero or more" regular expression connection that the T* spelling admits (and could we add T+ as a lightweight "one or more" spelling in the future...?).

On the whole, though, I don't think we need to optimize for super terse syntax here, so I'd be fine with the variadic T spelling as well.

Another thing I've been mulling over is whether we need to have a bright-line distinction between "type sequences" and "tuples". That is, if we add all the special operators (_countTypeSequence, _mapTypeSequence, etc.), would it be sufficient to say that a generic parameter T* declares a type T which stands for a tuple of arbitrary arity? Then, we could have a tuple-splat operator for turning a T value into its constituent parts (for e.g. function application). This might also dovetail nicely with @expanded to give us a way of expanding a variadic tuple into a parameter list, e.g.:

func variadic<Tuple*>(args: @expanded Tuple)
// or
func variadic<Tuple*>(args: #splat(Tuple))
// or, maybe 'Tuple' means the tuple version of the 
// type, and 'Tuple*' the splatted version
func variadic<Tuple*>(args: Tuple*)

all of these could indicate that variadic is callable as:

variadic("Hello", 1, 2.0)

This could also imply (though it wouldn't have to) that all the special type sequence operations would be applicable to fixed-size tuples as well:

for elt in ("Hello", 1, 2.0) {
  // elt is 'Any' here?
}

Is there any reason we need to treat type sequences as their own first-class "thing" as opposed to making them "just" tuples of variadic arity, and improving support for operating on arbitrary tuples?

11 Likes

Triple-dots already play many distinct roles in Swift’s grammar given that they are the spelling for variadics, a user-defined operator part, partial ranges, etc. It may make sense to adopt a different delimiter.

I've previously suggested using the triple-comma for variadics.

// Variadic parameters (or type sequences).
func debugPrint<Items,,,>(
  _ items: Items,,,
  separator: String = " ",
  terminator: String = "\n"
) where Items: CustomDebugStringConvertible

// Variadic arguments (or tuple-splat operations).
let items: (String, Int, Double) = ("Hi", 42, 3.14)
debugPrint(items,,,)
debugPrint(items,,, separator: "\t")
2 Likes

That's a neat suggestion (and I like it better than ... FWIW), but I think your example shows one potential issue with this syntax:

func debugPrint<Items,,,>(
  _ items: Items,,,        // <- syntax error here, or only ',,' required
  separator: String = " ", // in comma-separated lists?
  terminator: String = "\n"
) where Items: CustomDebugStringConvertible

This is, of course, a purely aesthetic preference, but I'm slightly bothered by both quadruple commas ,,,, and ,,, serving as both a "variadic" indicator and comma separator. I'd still prefer we choose something that doesn't visually with existing parameter list characters/syntax.

1 Like

The ASCII comma isn't available to custom operators, so single , and triple ,,, would both be valid list delimiters. (A quadruple ,,,, wouldn't be valid or necessary.)

1 Like

I'm excited for this work, and agree with many of the points made here about legibility. This is an advanced feature and we should consider less experienced developers encountering it for the first time. "Swift generic type postfix star" seems like an ineffective search term and I'd wager folks can come up with a bunch of equally SEO-unfriendly variations on this for each of the symbol-based approaches suggested here. Using a keyword seems much better in this regard, something like "Swift many expanded keywords" seems more likely to yield good results. Super +1 if we can come up with keywords that can be broadly understood without searching.
Also, because this is an advanced feature, I see little downside to erring on the side of verbosity. This is especially true when you consider the syntax it is replacing is 10 overloads of the same method with N type parameters each.


This may be a bit out-of-scope as this proposal explicitly excludes associated types, but I think the design would benefit from discussing how this would affect a potential implementation of static structural reflection. Something like:

protocol Structural {
  static var storedProperties: many StoredProperty<Self, _> { get }
}

allowing the user to define (using @Jumhyn's notation):

protocol OnlyIntProperties: Structural where forEach(T in storedProperties, T.Value == Int) {
}

It would be great if at least this specific use case could leverage semantics similar to whatever we decide on here (for the record, I'm not suggesting we implement this as part of this proposal, just that the semantics are extensible enough to support it).

3 Likes