What parameter packs solve
작성하는 모든 코드는 값과 타입이라는 두가지의 카테고리를 가지고 있다.
값은 다른 값을 매개변수로 받는 함수로 추상화할 수 있다.
func radians(from degrees: Double) -> Double
타입은 제네릭을 써서 추상화를 해줄 수 있다.
struct Array<Element>
두 경우 모두 구체적인 값이나 타입이 인자로 넘어가게 된다.
radians(from: 180)
Array<Int>
대부분의 제네릭 코드는 값과 타입 모두에 대해서 추상화 되어 있다.
제네릭 타입을 인자로 받는 함수가 이 경우다.
func query<Payload>(_ item: Request<Payload>) -> Payload
값은 여러개를 넣는 경우를 이미 지원하고 이를 가변 인자(variadic parameter)라 한다
func query(_ item: Request...)
하지만 가변 인자는 제한이 있다.
인자 갯수만큼의 원소를 가지는 튜플을 반환값으로 하고 싶은 경우는 이게 불가능하다.
func query(_ item: Request...) -> ???
타입을 지우지 않고서는 다양한 타입을 받을 수도 없다.
func query(_ item: AnyRequest...) -> ???
제네릭 시스템과 가변 인자에 없는 건 각 인자의 타입 정보를 유지하면서 인자의 갯수를 가변적으로 조절할 수 있는 기능이다.
지금은 함수 오버로딩을 여러개 하는 방법밖에 없다. 이러면 인자의 최대 갯수를 임의적으로 정해야만 한다.
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)
let _ = query(r1, r2, r3, r4, r5)
개념적으로 여러 인자를 다뤄야 하는 API를 만들 때 이런 패턴을 많이들 쓰고 그에 따른 한계도 공유한다.
이렇게 오버로딩을 하고 있다면, parameter pack이 필요하다는 강력한 신호다
How to read parameter packs
대부분은 한번에 하나의 값이나 타입을 다룬다.
parameter pack은 임의의 갯수의 타입이나 값을 가질 수 있고, 이를 하나로 패킹해서 함수의 인자로 넘긴다.
이렇게 개별 타입들을 묶어놓은 것을 type pack이라고 한다.
값을 묶어놓은 것은 value pack이다.
type pack과 value pack은 함께 쓰인다.
type pack은 value pack의 각 값들의 타입을 제공한다.
type pack과 value pack에서 같은 위치에 있는 구성요소는 서로 연관된다.
parameter pack을 통해서 pack안의 각 원소들에 대해서 동작하는 제네릭 코드를 한벌만 작성할 수있다.
이는 콜렉션을 사용할 때와 비슷한 개념이다.
for request in requests {
evaluate(request)
}
parameter pack에서 다른 점은 각 구성요소가 서로 다른 정적 타입을 가지고, 타입 레벨에서 코드를 작성할 수 있다는 것이다.
제네릭 코드는 보통 <> 사이에 타입 매개변수를 넣어서 작성한다.
<Payload>
Swift 5.9 부터는 type parameter pack을 each 키워드를 사용해서 선언할 수 있다.
<each Payload>
이제 함수는 타입 매개변수를 하나만 받지 않고, type parameter pack을 받는다.
func query<each Payload>
pack안의 개별 타입을 다루기 위해서는 ‘반복 패턴’을 사용한다.
repeat 키워드를 사용해서 표현되며 그 뒤에 ‘패턴 타입’이 따라온다
패턴 타입은 1개 이상의 pack 구성 요소에 대한 참조를 가진다.
repeat 키워드는 패턴 타입이 주어진 pack의 모든 구성요소에 대해서 반복될 것임을 나타낸다.
each는 각 반복마다 실제 pack의 구성요소 타입으로 치환될 placeholder 역할을 한다.
repeat Request<each Payload>
반복 패턴의 결과물은 콤마로 구분된 타입 리스트이므로, 콤마로 구분된 리스트가 들어갈 수 있는 곳에만 사용될 수 있다.
함수 매개변수로 쓰인 반복 패턴은 함수 인자를 value parameter pack으로 바꿔준다.
(_ item: repeat Request<each Payload>) -> Bool
덕분에 정의하는 곳에서의 인터페이스가 많이 간단해진다.
func query<each Payload>(_ item: repeat Request<each Payload>) -> (repeat each Payload)
let result = query(Request<Int>())
let results = query(Request<Int>(), Request<String>(), Request<Bool>())
동작 원리
실제 pack은 호출부를 통해서 유추된다.
// each Payload = Int, String, Bool
let results = query(Request<Int>(), Request<String>(), Request<Bool>())
실제 돌아가는 코드는 이 반복 패턴을 실제 타입으로 치환한 형태가 된다.
(Request<Int>, Request<String>, Request<Bool>) -> (Int, String, Bool)
제약걸기
func query<each Payload: Equatable>(
_ item: repeat Request<each Payload>
) -> (repeat each Payload)
func query<each Payload>(
_ item: repeat Request<each Payload>
) -> (repeat each Payload)
where repeat each Payload: Equatable
인자의 최소 갯수를 제한하기
제네릭 인자를 명시적으로 넣어주면 된다.
func query<FirstPayload, each Payload>(
_ first: Request<FirstPayload>, _ item: repeat Request<each Payload>
) -> (FirstPayload, repeat each Payload)
where FirstPayload: Equatable, repeat each Payload: Equatable
Using parameter packs
value pack 해제
struct Request<Payload> {
func evaluate() -> Payload
}
func query<each Payload>(_ item: repeat Request<each Payload>) -> (repeat each Payload) {
return (repeat (each item).evaluate()) // value pack의 구성요소를 가지는 튜플을 만든다.
}
심화
결과 값을 저장해놓기
Input과 Output을 다르게 하기
control flow 처리
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
}
}
}