WWDC Quick Look 💓 By SwiftGGTeam
Generalize APIs with parameter packs

Generalize APIs with parameter packs

Watch original video

Highlight

Swift 5.9 introduces parameter packs (Parameter Packs), useeachdeclare 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 containBoolIntStringThree separate types.

A value pack can hold any number of values. For example, a value package can containtrue10""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.

Comments

GitHub Issues · utterances