Will parameter packs gain upper limits?

Parameter packs perform a conversion of "()" to "(())" for function inputs, when an instance of Void needs to be returned.

func identity<each Element>(_ element: repeat each Element) -> (repeat each Element) {
  (repeat each element)
}

identity() == identity(()) // true

I can't tell if we're supposed to be imagining them as the intended sugar-fix for this old problem. Are we?

func identity(_ element: Void) { element }
identity(())
identity() // Missing argument for parameter #1 in call

Expanding further than 1 type, these don't mean the same thing…

()
((), ())
((), ((), ())
…

…but, given that you can impose a lower limit on the parameter type count, is an upper limit of 1 coming?


(This would also require the following constraint, which doesn't compile, but gives an error suggesting it will.)

where repeat each Element == Void { // Same-element requirements are not yet supported

It's not so much a conversion as the fact that () and (()) are different spellings of the same type, gotten by different means:

identity() // each Element == {} (a pack with zero elements)
identity(()) // each Element == {()} (a pack with one element of type `()`)
4 Likes

I'm trying to wrap my mind around why this is only true in the absence of more parameters.

func identity<Element0, each Element>(
  _ element0: Element0, _ element: repeat each Element
) -> (Element0, repeat each Element) {
  (element0, repeat each element)
}

identity(true, ()) as (Bool, Void) // compiles
identity(true) as (Bool, Void) // Cannot convert value of type 'Bool' to type '(Bool, Void)' in coercion

I thought maybe attempting these casts would help me understand, but they crash the compiler.

func identity<each Element>(_ element: repeat each Element) -> (repeat each Element) {
  (repeat each element)
}

_ = identity as () -> Void
_ = identity as (Void) -> Void

This is also made confusing by the fact that between the two cases, the visible parentheses in the expression (repeat each element) end up corresponding to different things in the resultant value. When each Element == {}, the parentheses constitute the empty tuple itself, whereas when each Element == {()} the parentheses are the outer bounds of an identity expression wrapping the empty tuple (which, of course, ends up evaluating to the empty tuple all the same).

Swift doesn't have a concept of single-element tuples that are distinct from their underlying type. So the thing that's 'special' here isn't the empty tuple type (), it's the single-element/identity type (T) which is always equivalent to T (even when T == ()). And this equivalence ceases to apply once you add additional elements.

5 Likes

We treat an unlabeled tuple of one element as being just a parenthesization of the one element type, so Foo, (Foo), ((Foo)), etc. are all the same type. When you expand a pack into a tuple type, and the result has one element in total, then you end up with the single parenthesized element type. So in your first case:

func identity<each Element>(_ element: repeat each Element) -> (repeat each Element)

when you call identity with zero arguments, then zero elements get expanded into the return type, and you end up with the empty tuple (). When you call identity with one () argument, that one () gets expanded into the tuple as (()), which is the same type as (). In your second example:

func identity<Element0, each Element>(
  _ element0: Element0, _ element: repeat each Element
) -> (Element0, repeat each Element) {
  (element0, repeat each element)
}

calling identity with element0 but no element arguments means that you expand no additional tuple elements into (Element0, repeat each Element), so you end up with (Element0), which is equivalent to Element0. So identity(true) as (Bool, Void) is a type mismatch, but identity(true) as Bool would work, since the result of the expansion is (Bool).

4 Likes

Not quite. The return type here is (repeat each Element). We can apply two substitutions:

  • Element := Pack{} gives you ().
  • Element := Pack{()} also gives you () because one-element tuples are unwrapped. For the same reason Element := Pack{Int} gives you Int.
  • Element := Pack{(), ()} gives you ((), ()), a tuple type of arity 2.
  • Element := Pack{Int, Int} gives you (Int, Int), another tuple type of arity 2.

These are all tuple types of different arity.

This requirement will totally be supported, but it means that each element of the Element pack is (), but the length of the pack is arbitrary. So if you have

func foo<each Element>(_: repeat each Element) where repeat each Element == ()

Then these are all valid ways to call foo():

foo()
foo(())
foo((), ())
foo((), (), ())
...

But you can't call foo() with any other argument type.

1 Like

Because T and (T) are always the same type.

That's a bug, both casts are supposed to succeed.

1 Like

It's just notional syntax for the substitution. So "Element := Pack{} gives you ()" means that when Element is an empty pack, (repeat each Element) is the empty tuple.

5 Likes

Yes, that’s what I’m getting at in the quoted portion—the identity function should be viewed as taking a parameter list and returning the result of wrapping that parameter list in parentheses. In the case of an empty parameter list, you get an empty list wrapped in parentheses, that is, (), the empty tuple. When you have two or more elements in the parameter list, you get back a tuple of those elements, e.g., (ā€œoneā€, 2).

However, when you have only a single element t in the parameter list, even if that single element is itself a tuple, what you get back is t wrapped in parentheses, and Swift treats t wrapped in parentheses as equivalent to t, so you don’t end up with the ā€˜additional level of tupling’ as in the other cases.

9 Likes