diff --git a/TSPL.docc/GuidedTour/GuidedTour.md b/TSPL.docc/GuidedTour/GuidedTour.md index 7f873312b..84ea87d2f 100644 --- a/TSPL.docc/GuidedTour/GuidedTour.md +++ b/TSPL.docc/GuidedTour/GuidedTour.md @@ -2380,6 +2380,17 @@ anyCommonElements([1, 2, 3], [3]) Writing `` is the same as writing ` ... where T: Equatable`. +Use `repeat` and `each` to make generic functions +where the number of arguments can vary. + +``` +func printEach(_ t: repeat each T) { + repeat print(each t) +} + +printEach(1, "hello", true) +``` + > Beta Software: > > This documentation contains preliminary information about an API or technology in development. This information is subject to change, and software implemented according to this documentation should be tested with final operating system software. diff --git a/TSPL.docc/LanguageGuide/Generics.md b/TSPL.docc/LanguageGuide/Generics.md index 5e5e9d417..3e6b8c330 100644 --- a/TSPL.docc/LanguageGuide/Generics.md +++ b/TSPL.docc/LanguageGuide/Generics.md @@ -1853,6 +1853,301 @@ protocol ComparableContainer: Container where Item: Comparable { } } --> +## Generic Parameter Packs + +To understand the problem that parameter packs solve, +consider the following overloads: + +```swift +func double(_ value: T) -> T { + return 2 * value +} + +func double(_ value1: T, _ value2: U) -> (T, U) { + return (2 * value1, 2 * value2) +} + +func double( + _ value1: T, + _ value2: U, + _ value3: V) -> (T, U, V) { + return (2 * value1, 2 * value2, 2 * value3) +} +``` + +Each function in the example above +takes a different number of arguments, +and returns a tuple containing the result of doubling those arguments. +Because the functions are generic, +they can take values of any numeric type, +and each argument can be a different type --- +and the doubled values that are returned have those same types. +Manually writing out each function like this +is repetitive and can be error prone. +It also imposes an arbitrary limit of three arguments, +even though the doubling operation could really apply +to any number of arguments. + +Other approaches that also have drawbacks +include taking the arguments as an array or a variadic parameter, +which requires all arguments to be the same type, +or using `Any` which erases type information. + +Parameter packs make it possible to abstract such function over the +number of generic arguments, eliminating the need to type-erase or +provide multiple overloaded copies of the same function with +a variable number of generic arguments. + +Writing this function with a parameter pack +preserves type information about its arguments, +and lets you call the function with an arbitrary number of arguments: + +```swift +func double(_ value: repeat each T) -> (repeat each T) { + // ... +} +``` + +In the code above, `each T` is declared in a generic parameter list. +It's marked with `each`, indicating that it's a type parameter pack. +In contrast to a generic type parameter, +which serves as a placeholder for a single type, +a type parameter pack is a placeholder for multiple types. +The ability to have `T` contain a varying number of types +is what allows this version of `double(_:)` +to take any number of parameters +while preserving the type information about each of its parameters. + +There isn't any syntax in Swift to write out the list of types, +but you use `repeat` and `each` +to mark code that is repeated for each value in that list. + +```swift +func double(_ value: repeat each T) -> (repeat each T) { + return (repeat (each value) * 2) +} + +let doubledNumbers = double(12, 0.5, 8 as Int8) +print(doubledNumbers) +// Prints (24, 1.0, 16) +``` + +The value of `doubledNumbers` is `(24, 1.0, 16)`, +and each element in this tuple has the same type +as the value in the value pack that it comes from. +Both `12` and `24` are `Int`, +`0.5` and `1.0` are `Double`, +and `8` and `16` are `Int8`. + +There are two types of parameter packs: type packs and value packs. The type +packs are made of type parameters, while value packs are made of values. + +At the call site of `double`, the type of the pack `T` will be substituted +for the type pack `{Int, Double, Int8}`. The type of the type pack is derived +from the value pack `12, 0.5, 8 as Int8` passed at the call site. This +guarantees that the corresponding types and values appear at the same +positions in their respective packs. + +The `double` function doubles each element of the parameter pack by applying +the `repeat` statement to each element of the value pack multiplied by 2. +The `repeat` statement applied to a value pack denotes a "repetition pattern", +which is a pattern that will be repeated for every value in the pack. + +Repetition is a great way to apply a simple transformation on each element +of the parameter pack. It effectively performs iteration on the value pack, +but in a clean and concise way. However, there are cases where your code either +demands a piece of other state to be updated on each iteration, or you want the +iteration to stop when a certain condition is met. + +Similarly to collections, value packs support iteration with a `for`-`in repeat` +loop. + +```swift +func allEmpty(_ array: repeat [each T]) -> Bool { + for a in repeat each array { + guard a.isEmpty else { return false } + } + + return true +} +``` + +In the code above, the `allEmpty` function is generic over a type parameter +pack `each T`, and takes a value parameter pack `array`, the type of which is +declared using `repeat [each T]` pack expansion, where `[each T]` is the +repetition pattern. + +The `for`-`in repeat` loop gives an opportunity to access elements of the value +pack on each iteration using a local variable. In this case, the local variable +`a` represents the value stored in the value pack on a given iteration. Using +`for`-`in repeat` loop gives you an opportunity to stop the iteration early—in +this case, the iteration will stop once a non-empty array is found. + +Here's how you might use the `allEmpty` function: + +```swift +print(allEmpty(["One", "Two"], [1], [true, false], [])) +// False +``` + +Using the `for`-`in repeat` loop is useful in cases where you want more control +over the pack iteration—stop the evaluation early for performance, or mutate +some other state on each iteration. + +Note that both approaches for iterating over the pack are fine! Choose one that +fits your use-case the most. + +> Note: Use singular naming convention when naming your parameter packs. + +Parameter packs can contain zero or more arguments. If you need to +require one or more, use a regular generic argument together with the type +pack argument: + +```swift +func requireOneOrMore(element: (T, repeat each U)) { } +``` + +The function above can now be called with at least one value passed into the +`element` parameter, required by the `T` generic argument. + +Repetition patterns can be applied to so many types! In the the `nonEmpty` +example above, the `array` parameter's type is declared with `repeat [each T]`, +expanding into multiple array types. + +A more complex type can include `each` multiple times. For example: + +```swift +func evaluateAll( + result: repeat Result +) -> [any Codable] { + // ... +} +``` + +The above `evaluateAll` function is generic over two different type packs, +`V` and `E`. The type of `result` is declared using the `Result` type that +takes 2 generic parameters. `repeat Result` expands the +type of the `result` parameter to be a type pack where the expansion of +`each V` and `each E` is required to be of the same size, meaning the +number of types both packs expand to are guaranteed to be equal. + +You can also vary the return types of your function based on the parameter +types: + +```swift +protocol RequestProtocol { + associatedtype Input + associatedtype Output + func evaluate(_ input: Input) -> Output +} + +struct Evaluator { + var item: (repeat each Request) + + func query(_ input: repeat (each Request).Input) -> (repeat (each Request).Output) { + return (repeat (each item).evaluate(each input)) + } +} +``` + +In the above code example, the protocol `RequestProtocol` requires a function +`evaluate` that, given an input of an associated type `Input`, returns the +other associated type `Output`. + +The `Evaluator` struct is generic over the `Request` type, which is a parameter +pack. Like with regular generics, parameter packs support type constraints—each +element in the `Request` type pack has to conform to the `RequestProtocol` +protocol. + +The `item` property stores the elements of the `Request` parameter +pack in a tuple. + +The `query` methods operates on an `input` argument of +`repeat (each Request).Input` type. Because each element of the `Request` +type pack conforms to `RequestProtocol`, the implementation of `query` is able +to call the protocol requirement `evaluate`, using the elements of the `input` +parameter pack as its input. As a result, a new pack containing +`RequestProtocol.Output` type is produced. + +You don't always write `repeat each` one after the other. The part to repeat +is marked `repeat` and the location of the element from the pack is marked +`each`. In this example, the `repeat` +keyword applied in front of `(each item).evaluate(each input)` call +calls `evaluate` function on each pair of elements from `item` and `input` +value packs, respectively. Again, such construction guarantees that `item` +and `input` packs have to be of the same length. As a result, the tuple +returned by `query` also has the same length. + +You can also use trailing `where` clause with parameter packs in more complex +cases. For example, consider the following type: + +```swift +struct Container { + var value: (repeat each Element) +} +``` + +With the `where` clause, it is easy to conditionally constrain the type to +conform to protocols using the types in the `Element` parameter pack: + +```swift +extension Container: Sendable where repeat each Element: Sendable {} +``` + +In the code above, `Container` conditionally conforms to `Sendable` when each +element of the type parameter pack `Element` conforms to `Sendable`, making it +usable in contexts requiring sendability. + +The types that appear in a type parameter pack must be constrained if its +expansion is used as a constrained generic type parameter. For example, +types such as `Set` require its generic argument to conform to `Hashable`, +so each element of the parameter pack needs to be constrained: + +```swift +struct Container { + var value: (repeat each Set) +} +``` + +Using all of the concepts above, parameter packs provide powerful capabilities +in your code. Let's consider a more advanced example of using parameter packs, +and start off by declaring the following protocol: + +```swift +protocol ValueProducer { + associatedtype Value: Codable + func evaluate() -> Value +} +``` + +The above protocol `ValueProducer` requires the `evaluate()` method that’s +return type is the associated type `Value` that conforms to `Codable` protocol. + +For example, suppose you get a parameter pack of values of type +`Result`, and you need to iterate only over the success +elements and call the `evaluate()` method on its value. Also, suppose you need +to save the result of each call into an array. + +```swift +func evaluateAll(result: repeat Result) -> [any Codable] { + var evaluated: [any Codable] = [] + for case .success(let valueProducer) in repeat each result { + evaluated.append(valueProducer.evaluate()) + } + + return evaluated +} +``` + +In the body of `evaluateAll`, you can use the `for`-`in repeat` loop +for performing an operation while +iterating over the values stored in the `result` value pack. Note that you +can use a familiar `for case` pattern to only iterate over the `.success` +case of the `Result` enum. You can bind the value using the `valueProducer` +variable. Finally, on each iteration, you can append the result of the call +to the `evaluate()` method to the `evaluated` array, which gets returned +from the function. + ## Generic Subscripts Subscripts can be generic, diff --git a/TSPL.docc/ReferenceManual/Expressions.md b/TSPL.docc/ReferenceManual/Expressions.md index c72d9a67e..3502f4a36 100644 --- a/TSPL.docc/ReferenceManual/Expressions.md +++ b/TSPL.docc/ReferenceManual/Expressions.md @@ -1477,6 +1477,49 @@ A single expression inside parentheses is a parenthesized expression. > *tuple-element-list* → *tuple-element* | *tuple-element* **`,`** *tuple-element-list* \ > *tuple-element* → *expression* | *identifier* **`:`** *expression* +### Parameter-Pack Expression + +XXX OUTLINE: + +- A `repeat` expression must contain one or more `each` expressions + and has the shape `repeat <#repetition pattern#>` + +- TODO list of contexts where expansion is supported + + + in a tuple, producing tuple elements + + as a statement, including at top level, repeating the statement's expression + + but not in a function call, producing arguments + + in a `for`-`each` loop + +- The *repetition pattern* is repeated once for each type in the pack + +- If an expression includes both the `repeat` operator and + a `try` or `await` operator, + the `repeat` operator must appear first. + (So `repeat try each foo` or `repeat each try foo`) + +- All of the `each` expressions in a pattern expression + must expand packs that have the same number of types. + +- In a function declaration, + an argument whose type is a parameter-pack expansion type + must be the last parameter + or the parameter after it must have a label. + +- It's valid for a pack expression to contain no elements, + in which case the parameter-pack expansion expression isn't evaluated at all (zero times) + + + +> Grammar of a pack-expansion expression: +> +> *parameter-pack-expression* → **`each`** *expression* +> +> *parameter-pack-expansion-expression* → **`repeat`** *expression* + ### Wildcard Expression A *wildcard expression* diff --git a/TSPL.docc/ReferenceManual/Statements.md b/TSPL.docc/ReferenceManual/Statements.md index 8f9913cf7..4f7e9597d 100644 --- a/TSPL.docc/ReferenceManual/Statements.md +++ b/TSPL.docc/ReferenceManual/Statements.md @@ -70,17 +70,19 @@ and a `continue` statement and is discussed in A `for`-`in` statement allows a block of code to be executed once for each item in a collection (or any type) that conforms to the -[`Sequence`](https://developer.apple.com/documentation/swift/sequence) protocol. +[`Sequence`](https://developer.apple.com/documentation/swift/sequence) protocol, +or in a value parameter pack. A `for`-`in` statement has the following form: ```swift -for <#item#> in <#collection#> { +for <#item#> in <#expression#> { <#statements#> } ``` -The `makeIterator()` method is called on the *collection* expression +If the *expression* of a `for`-`in` statement is a collection *expression*, +the `makeIterator()` method is called on it to obtain a value of an iterator type --- that is, a type that conforms to the [`IteratorProtocol`](https://developer.apple.com/documentation/swift/iteratorprotocol) protocol. @@ -93,6 +95,21 @@ and then continues execution at the beginning of the loop. Otherwise, the program doesn't perform assignment or execute the *statements*, and it's finished executing the `for`-`in` statement. +The *expression* of a `for`-`in` statement +may also be a pack expansion *expression*. +In this case, +on the *i*th iteration, +the type of *item* is the *i*th type parameter +in the type parameter pack being iterated over. +The value of *item* is the pattern type of *expression*, +with each captured type parameter pack replaced with +an implicit scalar type parameter with matching requirements. +The *expression* is evaluated once at each iteration, +instead of `n` times eagerly, +where `n` is the length of the packs captured by the pattern. +The program will continue executing *statements* +while the total length of the packs captured by the pattern isn't reached. + > Grammar of a for-in statement: > > *for-in-statement* → **`for`** **`case`**_?_ *pattern* **`in`** *expression* *where-clause*_?_ *code-block* diff --git a/TSPL.docc/ReferenceManual/Types.md b/TSPL.docc/ReferenceManual/Types.md index 710f61944..2d77cca71 100644 --- a/TSPL.docc/ReferenceManual/Types.md +++ b/TSPL.docc/ReferenceManual/Types.md @@ -46,6 +46,7 @@ and describes the type inference behavior of Swift. > *type* → *protocol-composition-type* \ > *type* → *opaque-type* \ > *type* → *boxed-protocol-type* \ +> *type* → *parameter-pack-type* > *type* → *metatype-type* \ > *type* → *any-type* \ > *type* → *self-type* \ @@ -948,6 +949,51 @@ https://github.com/apple/swift-evolution/blob/main/proposals/0335-existential-an > > *boxed-protocol-type* → **`any`** *type* +## Type Parameter Pack + +XXX OUTLINE: + +- `each T` creates a type parameter pack + when it appears in a generic parameter clause, + or indicates which pack should be expanded + when it appears in a `repeat` expression + +- `repeat P`, where `P` captures at least one type + parameter pack, expands it into a list of types + or values + +- Packs are never nested; expansion implies flattening + +- To expand the values, you use a parameter pack expression; + see + +- It's valid for a type pack to contain no elements. + +- list of places where type parameter pack can appear: + + + generic type parameter list + + as an argument to a generic type parameter + + tuple element + + function type parameter list + +> Grammar of a type-parameter pack: +> +> *type-parameter-pack* → **`each`** *type* +> +> *type-parameter-pack-expansion* → **`repeat`** *pattern-type* +> +> *pattern-type* -> *type* + + + ## Metatype Type A *metatype type* refers to the type of any type,