• 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를 만들 때 이런 패턴을 많이들 쓰고 그에 따른 한계도 공유한다.

      • 중복 문제도 있고, 상한선이 임의대로 정해지게 된다.
      • 이 상한선을 넘어서면 extra argument 컴파일 에러가 난다.
    • 이렇게 오버로딩을 하고 있다면, parameter pack이 필요하다는 강력한 신호다

  • How to read parameter packs

    • 대부분은 한번에 하나의 값이나 타입을 다룬다.

    • parameter pack은 임의의 갯수의 타입이나 값을 가질 수 있고, 이를 하나로 패킹해서 함수의 인자로 넘긴다.

      • 이렇게 개별 타입들을 묶어놓은 것을 type pack이라고 한다.

        스크린샷 2023-06-16 오전 12.54.01.png

      • 값을 묶어놓은 것은 value pack이다.

        스크린샷 2023-06-16 오전 12.54.33.png

    • type pack과 value pack은 함께 쓰인다.

      • type pack은 value pack의 각 값들의 타입을 제공한다.

      • type pack과 value pack에서 같은 위치에 있는 구성요소는 서로 연관된다.

        스크린샷 2023-06-16 오전 12.56.22.png

    • 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
      
    • 덕분에 정의하는 곳에서의 인터페이스가 많이 간단해진다.

      • 여러개의 오버로드가 하나로 합쳐진다.
      • 함수 매개 변수와 반환값이 같은 type parameter pack에 의존하고 있기 때문에, 인자로 들어온 매개변수의 갯수와 반환 tuple의 길이는 항상 동일하다.
      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은 호출부를 통해서 유추된다.

        • placeholder를 대체하고 있는 구체 타입은 type 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
                }
            }
        }