Highlight
Swift 5.9 introduces parameter packs (Parameter Packs), use
eachdeclare type parameter pack,repeatDefine repeating patterns to allow a single generic function to abstract both the number of parameters and their types, completely eliminating the boilerplate code of N function overloads.
Core Content
Swift’s generics system already abstracts types very well.Array<Element>Can accommodate any type,query<Payload>(_ item: Request<Payload>) -> PayloadCan handle any request type.
But problems arise when you need to initiate multiple requests at the same time. Each request may return a different type, and you want the result to be a tuple, with the type in each position corresponding to the type of the request.
(02:04)
Overload Hell
The current approach is to write multiple overloads:
func query<Payload>(_ item: Request<Payload>) -> Payload
func query<Payload1, Payload2>(
_ item1: Request<Payload1>,
_ item2: Request<Payload2>
) -> (Payload1, Payload2)
func query<Payload1, Payload2, Payload3>(
_ item1: Request<Payload1>,
_ item2: Request<Payload2>,
_ item3: Request<Payload3>
) -> (Payload1, Payload2, Payload3)
func query<Payload1, Payload2, Payload3, Payload4>(
_ item1: Request<Payload1>,
_ item2: Request<Payload2>,
_ item3: Request<Payload3>,
_ item4: Request<Payload4>
) -> (Payload1, Payload2, Payload3, Payload4)
Four overloads, the code is almost the same. If you pass five parameters, the compiler will report an error. Worse, this overloading pattern is ubiquitous in both the Swift standard library and third-party libraries.
(03:23)
Concept of parameter package
Parameter packages introduce two new concepts:
A type pack (type pack) can hold any number of types. For example, a type package can containBool、Int、StringThree separate types.
A value pack can hold any number of values. For example, a value package can containtrue、10、""three independent values.
Type packages and value packages are used together: each type in the type package corresponds to the value at the same position in the value package. Position 0 oftrueThe type isBool, position 1 of10The type isInt, position 2 of""The type isString。
(04:41)
Rewrite query with parameter package
After using parameter pack, N overloads are compressed into one function:
func query<each Payload>(
_ item: repeat Request<each Payload>
) -> (repeat each Payload)
Key points:
each PayloadDeclare a type parameter pack -repeat Request<each Payload>Indicates repeated occurrences in the parameter listRequest<each Payload>repeat each PayloadRepresents repeated occurrences in the returned tupleeach Payload
It’s completely natural when called:
let result = query(Request<Int>())
// result has type Int
let results = query(Request<Int>(), Request<String>(), Request<Bool>())
// results has type (Int, String, Bool)
The compiler expands parameter packs at compile time to generate type-safe code. You don’t need to write any overloads.
(09:37)
Detailed Content
Syntax rules for parameter packages
Used to declare type parameter packageseachKeywords:
func query<each Payload>(
_ item: repeat Request<each Payload>
) -> (repeat each Payload)
repeatCan only be used in comma-separated lists: tuple types, function parameter lists, generic parameter lists. Cannot be used anywhere.
Constraints can be added to parameter packages:
// All types conform to Equatable
func query<each Payload: Equatable>(
_ item: repeat Request<each Payload>
) -> (repeat each Payload)
// Or use a where clause
func query<each Payload>(
_ item: repeat Request<each Payload>
) -> (repeat each Payload)
where repeat each Payload: Equatable
It is also possible to mix ordinary generic parameters and parameter packs:
func query<FirstPayload, each Payload>(
_ first: Request<FirstPayload>,
_ item: repeat Request<each Payload>
) -> (FirstPayload, repeat each Payload)
where FirstPayload: Equatable, repeat each Payload: Equatable
(11:03)
Implementation of parameter package
Implementation of parameter pack functionrepeatto iterate over each element:
struct Request<Payload> {
func evaluate() -> Payload
}
func query<each Payload>(
_ item: repeat Request<each Payload>
) -> (repeat each Payload) {
return (repeat (each item).evaluate())
}
Key points:
each itemUnpack each element in the value bag -(each item).evaluate()Call method on each element -repeat (...)Pack the results into a value package- Outer parentheses
(repeat ...)Assemble value packages into tuples
(13:42)
More complex example
Parameter packs also work when the input and output types are different:
protocol RequestProtocol {
associatedtype Input
associatedtype Output
func evaluate(_ input: Input) -> Output
}
struct Evaluator<each Request: RequestProtocol> {
var item: (repeat each Request)
func query(
_ input: repeat (each Request).Input
) -> (repeat (each Request).Output) {
return (repeat (each item).evaluate(each input))
}
}
here:
-each Requestis a type parameter pack, each type followsRequestProtocol
(each Request).InputGet the Input associated type for each request type -(each Request).OutputGet the Output associated type for each request type -each inputandeach itemUnpack the input value package and request value package separately -(each item).evaluate(each input)Pass the corresponding input to the corresponding request
(16:04)
Parameter package with control flow
Parameter packs can also be combined withtryUse together with:
protocol RequestProtocol {
associatedtype Input
associatedtype Output
func evaluate(_ input: Input) throws -> Output
}
struct Evaluator<each Request: RequestProtocol> {
var item: (repeat each Request)
func query(
_ input: repeat (each Request).Input
) -> (repeat (each Request).Output)? {
do {
return (repeat try (each item).evaluate(each input))
} catch {
return nil
}
}
}
repeat try (...)Indicates that it is applied to each elementtry. If any request throws an error, the entiredoblock entrycatch。
(17:05)
Core Takeaways
Refactor your network layer API
If your project has multiplefetchMethod overloads (1 parameter, 2 parameters, 3 parameters…) are combined into one using parameter packs. The caller experience remains unchanged and maintenance costs are significantly reduced.
Entrance: Putfunc fetch<T1, T2>(_ r1: Request<T1>, _ r2: Request<T2>) -> (T1, T2)Replace the overload withfunc fetch<each T>(_ request: repeat Request<each T>) -> (repeat each T)。
Type-safe dependency injection container
Implement one using parameter packageContainer.resolve()Method to resolve multiple dependencies at the same time:
let (db, network, cache) = container.resolve(Database.self, Network.self, Cache.self)
Entry: Definitionfunc resolve<each T>() -> (repeat each T), find the corresponding instance according to the type inside the container.
Uniform encapsulation of multiple concurrent requests
In SwiftUI or Combine projects, there are often scenarios where multiple requests are initiated at the same time and the UI is updated after all are completed. Encapsulate a parameter packageconcurrentfunction:
let (user, posts, settings) = await concurrent(
fetchUser(),
fetchPosts(),
fetchSettings()
)
Entrance:func concurrent<each T>(_ task: repeat @Sendable () async -> each T) async -> (repeat each T), for internal usewithTaskGroupConcurrent execution.
Generic tools for tuple types
write azipfunction, put multipleOptionalpackaged into oneOptionaltuple:
let result = zip(opt1, opt2, opt3)
// result: (T1, T2, T3)?
Entrance:func zip<each T>(_ value: repeat each T?) -> (repeat each T)?, returns a tuple when all values are non-nil, otherwise returns nil.
Related Sessions
- Write Swift macros — Write your first Swift macro
- Expand on Swift macros — Deeply understand the design principles of macros and more role types
- Swift concurrency: Behind the scenes — The underlying principles of Swift structured concurrency
- Meet Swift OpenAPI Generator — Use code generation to simplify server-side API calls
Comments
GitHub Issues · utterances