Pitching The Start of Variadic Generics

<T...> where T[i], (2 ... 10).contains(i), T[5] == String

Ranges are good, but here I think it should say that i is in the range 0..<n, where n is in (2...10).

This seems to be similar to the idea I suggested in my reply above too: Pitching The Start of Variadic Generics - #18 by AliSoftware

TL;DR: My suggested syntax would be:

func debugPrint<T[]>(_ values: T[])
  where T: CustomDebugStringConvertible
{
  print(values.count)
  for v in values { /* v is Any */ }
}
func needAtLeastOne<T[1...]>(_ values: T[]) {
  ...
}
func debugPrintEquivalentSyntax1<T[0...]>(_ values: T[])
  where T: CustomDebugStringConvertible
{ ... }
func debugPrintEquivalentSyntax2<T[...]>(_ values: T[]) // '...' here is the UnboundedRange operator
  where T: CustomDebugStringConvertible
{ ... }

And

public struct ZipSequence<Sequences[]>: Sequence
  where Sequences: Sequence 
{
   ...
   public struct Iterator: IteratorProtocol {
      public typealias Element = Sequences[].Element // maps the 'Sequences' type sequence into a type sequence of their respective 'Element'
   }
}

What I like about this syntax is:

  • It plays well with potential future direction of specifying arity and arity ranges
  • The square brackets makes it more natural (at least to me) to see and read T[0], T[1], T[n] at being different types (and not necessarily all the same like T... can imply due to its ressemblance with the existing variadic parameters feature)
1 Like

Is there really a value in adding ranges? It seems to make more sense defining explicitly typed parameters with trailing variadic generics. I can't think of any example where indexing variadic types is more clear than providing explicit types.

If we want a single string, do something like this: func foo<T...>(a: String, b: T...). This could even cover more complex cases like: func foo<U..., V...>(a: U...) where U == (String, V...).

I'm not quite able to follow your point, but when I was talking about ranges I was referring to the dimension. Personally I only really care about cases with a fixed dimension, which would be something like func f<variadic T[5]>(T) { } but you could imagine wanting something to be defined for a dimension of 3, 4 or 5 for example: func f<variadic T[3...5]>(T) { }.

Sorry, I think I replied to the wrong comment. I think I meant to reply to @DevAndArtist, or whoever started the discussion about defining certain type conformances at certain indices. I was assuming T[5] == String Is trying to bind the element at index 5 to a string, or T[3…5] == SomeProtocol To define conformance for a certain sub range.

I was wondering what advantages this has over creating explicitly typed parameters, or multiple variadic generic groups.

1 Like

I agree with you. If specific index or indices like T[5] (6th type parameter) have special meaning and it should be String, it means you should split them into different parameters, like <T0, T1, T2, T3, T4, Rest...>. Constraints like where 2 <= n have similar problem. It would mean, certain two type parameters have special meaning. Therefore, perhaps you should use perameter-split strategy like <First, Second, Rest...>, <First, Middle..., Last> or other corresponding signature for your intention.

Nevertheless, I think some type of constraints cannot be expressed without subscript-like syntax. (using @Jumhyn's forEach like syntax).

// declare fixed-length version of chain
static func buildBlock<A, B, C, D>(_ f: (A) -> B, _ g: (B) -> C, _ h: (C) -> D) -> A -> D {
    let f0 = { (value: A) in value }        // f0: (A) -> A
    let f1 = { (value: A) in f(f0(value)) } // f1: (A) -> B
    let f2 = { (value: A) in g(f1(value)) } // f2: (A) -> C
    let f3 = { (value: A) in h(f2(value)) } // f3: (A) -> D
    return f3
}

// variadic version
static func buildBlock<type(n) Input, types(n) Output>(
//                     ~~~~~^~~~~~     ~~~~~^~~~~~   
//              specify exact number of type parameters
    _ closures: ((Input) -> Output)...
) -> (Input[0]) -> Output[n-1] where 1 <= n, forEach(i, i+1 in 0 ..< n) Input[i+1] == Output[i]
//                                                   ~~~~~~~~^~~~~~~~~
//                                             ensure i and i+1 in valid range 

I tried but couldn't write this function without <= and subscript access. When I add FirstInput and LastOutput to remove <= and(Input[0]) -> Output[n-1], I couldn't write the signature of closures. I think subscript like range access would also have such usage in some rare conditions.

By the way, it also means that further discussion is required about how to treat variadic values. The pitch describes map , but reduce like operation would be also required. Chain would not be able to be expressed with only map and for .

static func buildBlock<type(n) Input, types(n) Output>(
    _ closures: ((Input) -> Output)...
) -> (Input[0]) -> Output[n-1] where 1 <= n,  forEach(i, i+1 in 0 ..< n) Input[i+1] == Output[i] {
    // how to chain closures here...?
}

(EDIT) I was replying to the wrong address. This is not a reply to @nonsensery, I'm sorry :bowing_man:
This is a reply to @maustinstar.

1 Like

I never thought that variadic parameters deserved the shorthand ... syntax and variadic generics are likely to be even less used than that. Perhaps consider an attribute in front of the type parameter (e.g. @variadic), which might also be a useful place to specify the additional constraints being discussed here, like arity.

2 Likes

As for the potential name of a keyword. An alternative name that was floating around in past thread was pack.

We could also explore the possibility to unfold a pack through a where clause.

<pack T> where T == <_, T1, ...>, T1 == String

Building on the keyword idea, in the future we might improve the generics UX and potentially be able to express things like:

func foo(_: pack T) 

// similar to
func bar(_: some T)
3 Likes

I think I really like this notation. Is the T0 and T1 implied from the T or would they be arbitrary placeholders? Would <pack T> where T == <ID, ...>, ID == Equatable be valid?

I would argue that this would be valid, however it should be probably == any/some Equatable or ID: Equatable, but that me nit picking on something else.

I would probably add the following to the pitched design:

  • the T == <…> syntax would be unfolding / unpacking the variadic pack T
  • T[n] could still be valid as <pack T> where T[5] == String would reference the type inside the pack; it does not violate or collide with the idea of static subscripts, because T in that context wouldn‘t be specific type, but rather a name of the variadic pack

Hence I think this could still be valid:

func foo<pack T[n]>(_: T…) 
  where 
  T == <A, B, B, _, Int, …>, 
  A: Equatable, 
  B == String, 
  T[5] == Bool,
  n >= 6 && n <= 10

// notice that `A`, `B`, and `C` were generic types,
// while the 5th type was already explicitly set to `Int`!
// we could allow this, but it‘s debatable.
// furthermore the 4th type used a “placeholder type“ `_`

foo(true, "foo", "bar", 1, 1, false, /* … */) 

The subscript syntax is really nice if you need to express very complex constraints where you know or need to require specific pack size. I personally wouldn‘t scrap that idea.


TLDR;

We could introduce these things in multiple phases:

  1. Introduce just pack T
  2. Introduce the ability to unpack where T == <…>
  3. Introduce access via pack index pack T[n] and sizes where n < 10

Each of the phases would require its own discussion and proposal, and this current thread should probably just focus on #1.

<pack T> where T == <_, T1, ...>, T1 == String

This seems a bit illogical though. On the left it seems that T is a placeholder for an individual type in the pack, not the pack itself, but after where it is treated as the totality. If we want that kind of control I think something like

<pack _, T1, ...> where T1 == String

would make more sense.

I read this as “placeholder type as a pack, followed by T1 which is a string, followed by dots which mean nothing to my brain“. No offense though. If anything this should maybe be written as.

<pack <_, T1, …>> where T1 == String

The unpacking syntax can extract the generic types from the pack and achieve the same as the index syntax. Both do partly overlap and this is expected.

<pack T> where T == <_, R, …>, R == String
<pack T> where T[1] == String // the same as above 

In the above example the index syntax is shorter, but we can find the opposite example as well.

<pack T> where T == <Int, Bool, String, Double, …>
<pack T> where T[0] == Int, T[1] == Bool, T[2] == String, T[3] == Double

It was just an example, I'm not proposing my syntax. I just wanted to point out the inconsistency in what T means. Feel free to put brackets around the argument to pack in my example!

This does not suffer from the inconsistency though:

<pack T> where T[1] == String

And it could be amended with things like T.count == 5. Not sure how to indicate that all individual Ts are equal though. Perhaps just

<pack Double>

I prefer variadic to pack though.

Come to think of it, why not just this?

<variadic T: Int, Bool, String, Double, …>

Again, please don‘t be offended. I‘m just pitching brainstormed syntax and do not claim it to be superior to any other options. ;) I also just provided my 2cents on how I read the syntax in your example.

: always meant sub-typing, inheritence or protocol conformance to me. That‘s why in generics UX threads we kept referring to same type constraint as any Collection<.Element == String>.

I‘ve updated my initial response with a potential fix to your syntax.

Would any T instead of T... make any sense? Such naming follows exact meaning of such generic parameter. Just asking.

Could that last example be rewritten like this?

<pack T> where T == <Int, Bool, String, Double, …>
<pack T> where T[0...3] == (Int, Bool, String, Double)
<pack T> where T[0...3] == <Int, Bool, String, Double> // or this, if the above line looks too tupleish :-)

It's still longer, but not by nearly as much.

Not sure, maybe, but to me personally it reads that T is required to be a tuple.

As I previously stated, I think every part of this should have its own proposal where we can discuss all the syntax options.

I would probably argue that a range like syntax can also allow partial specialization of the types:

// T can still be 0 or infinite
<pack T> where T[3...6] == <Int, Bool, String, Double>

Fair point. Edited

1 Like

We had a similar idea in the end ;)

1 Like

:rofl: We got caught in an editing race! I think I saw the page shift right before I submitted, so I suppose you won :grin:

2 Likes