ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Iterate Over Parameter Packs
    Swift 2025. 10. 18. 09:32

    안녕하세요. 그린입니다 🍏

    이번 포스팅에서는 Swift 6.0에서 도입된 Pack Iteration에 대해 정리해보려 합니다.

     

    Swift 5.9에서 도입된 Parameter Packs를 훨씬 더 쉽고 직관적으로 다룰 수 있게 해주는 기능인데요, 실제 코드 예제와 함께 살펴보겠습니다 🙋🏻


    Parameter Packs Recap

    먼저 Parameter Packs가 무엇인지 간단히 복습해볼까요?

    다음 코드를 보겠습니다.

     

    let areEqual = (1, true, "hello") == (1, false, "hello")
    print(areEqual)
    // false
    

     

     

    위 코드는 단순히 두 튜플을 비교합니다.

    하지만 이 코드는 튜플에 7개의 요소가 있으면 작동하지 않았습니다!

     

    Swift 표준 라이브러리는 오랫동안 최대 6개 요소까지의 튜플에 대해서만 비교 연산자를 제공했습니다.

     

    func == (lhs: (), rhs: ()) -> Bool
    func == <A, B>(lhs: (A, B), rhs: (A, B)) -> Bool where A: Equatable, B: Equatable
    func == <A, B, C>(lhs: (A, B, C), rhs: (A, B, C)) -> Bool where A: Equatable, B: Equatable, C: Equatable
    // 이런 식으로 6개 요소 튜플까지만...
    

     

    위의 각 제네릭 함수에서는 입력 튜플의 모든 요소가 함수의 제네릭 매개변수 목록에 타입이 선언되어야 합니다.

    따라서 더 큰 튜플 크기를 지원하려면 제네릭 매개변수 목록에 새 요소를 추가해야 했죠.

    이 때문에 6개 요소 튜플이라는 인위적인 제한이 걸렸어요.

     


    Parameter Packs의 등장

    Parameter Packs는 가변 개수의 타입 매개변수에 대해 함수를 추상화하는 기능을 추가했습니다.

     

    이를 통해 다음과 같이 작성된 == 연산자를 사용하여 6개 요소 제한을 해제할 수 있습니다.

     

    func == <each Element: Equatable>(lhs: (repeat each Element), rhs: (repeat each Element)) -> Bool

     

    위 시그니처에서 볼 수 있는 타입들을 분해해볼까요.

    1. each Element

    • 제네릭 매개변수 목록의 each Element를 봐야합니다.
    • each 키워드는 Element가 타입 매개변수 팩(type parameter pack)임을 나타냅니다.
    • 즉, 임의의 개수의 제네릭 인자를 받을 수 있다는 의미입니다.
    • 일반 제네릭 매개변수처럼 타입 매개변수 팩에도 준수 요구사항을 선언할 수 있습니다.
    • 이 경우 각 Element 타입이 Equatable 프로토콜을 준수하도록 요구합니다.

    2. repeat each Element

    • 이 함수는 두 개의 튜플 lhs와 rhs를 인자로 받습니다.
    • 두 경우 모두 튜플의 요소 타입은 repeat each Element입니다.
    • 이를 팩 확장 타입(pack expansion type)이라고 부릅니다.
    • repeat 키워드 다음에 반복 패턴(repetition pattern)이 오며, 이 패턴은 팩 참조를 포함해야 합니다.
    • 우리의 경우 반복 패턴은 each Element입니다.

    3. 런타임 동작

    • 호출 시점에 사용자는 각 튜플에 대한 값 매개변수 팩을 제공하며, 이는 해당 타입 매개변수 팩으로 대체됩니다.
    • 런타임에 반복 패턴은 대체된 팩의 각 요소에 대해 반복됩니다.

    호출 예시

    let areEqual = (1, true, "hello") == (1, false, "hello")
    print(areEqual)
    // false
    

     

    == 호출은 타입 팩 {Int, Bool, String}을 Element 타입 팩으로 대체합니다. 

    lhs와 rhs 모두 동일한 타입을 가집니다.

    마지막으로 == 함수는 lhs 튜플의 값 팩 {1, true, "hello"}와 rhs 튜플의 값 팩 {1, false, "hello"}로 호출됩니다.


    왜 Pack Iteration이 필요한가?

    튜플 비교 연산자의 새로운 시그니처는 꽤 나이스해 보이지만, 실제로 함수 본문 내에서 lhs와 rhs 튜플의 값을 어떻게 사용할까요?

    Swift 6.0 이전의 구현

    Swift 6.0 이전에는 이 함수를 간결하게 구현할 방법이 없었습니다.

    한 가지 해결책은 두 튜플의 요소 쌍을 비교하는 로컬 함수를 만들고, 팩 확장을 사용하여 모든 요소 쌍에 대해 해당 함수를 호출했습니다.

     

    struct NotEqual: Error {}
    
    func == <each Element: Equatable>(lhs: (repeat each Element), rhs: (repeat each Element)) -> Bool {
        // 팩 확장의 각 요소를 처리하기 위한 로컬 throwing 함수
        func isEqual<T: Equatable>(_ left: T, _ right: T) throws {
            if left == right {
                return
            }
            throw NotEqual()
        }
        
        // 두 튜플 요소가 같지 않으면 즉시 false를 반환하기 위한 do-catch 문
        do {
            repeat try isEqual(each lhs, each rhs)
        } catch {
            return false
        }
        return true
    }
    

     

    위 코드는 좋아 보이지 않죠?

    각 요소 쌍에 대해 조건을 확인하기 위해 주어진 요소를 비교하는 로컬 함수 isEqual을 선언해야 합니다.

    하지만 이것만으로는 함수가 조기에 반환되도록 만들기에 충분하지 않습니다.

    왜냐하면 팩을 확장할 때 로컬 isEqual 함수가 매개변수 팩 lhs와 rhs의 모든 요소 쌍에 대해 여전히 호출되기 때문입니다.

    이 때문에 isEqual을 throws로 표시하고 일치하지 않는 요소 쌍을 찾으면 에러를 던져야 합니다.

    그런 다음 catch블록에서 에러를 잡아 false를 반환합니다.

     

    복잡하고 직관적이지 않죠? 😓

     

    Pack Iteration의 도입

    Swift 6.0은 익숙한 for-in 루프 구문을 사용하는 pack iteration을 도입하여 이 작업을 크게 단순화합니다.

    pack iteration을 사용하면 == 튜플 비교 연산자의 본문이 간단한 for-in repeat 루프로 단순화됩니다.

     

    func == <each Element: Equatable>(lhs: (repeat each Element), rhs: (repeat each Element)) -> Bool {
        for (left, right) in repeat (each lhs, each rhs) {
            guard left == right else { return false }
        }
        return true
    }
    

     

    위 코드에서 for-in 루프 기능을 활용하여 튜플을 쌍으로 반복할 수 있습니다.

    팩을 반복할 때는 새로운 for-in repeat 구문을 사용하며, 그 뒤에 반복하려는 값 매개변수 팩이 옵니다.

    매 반복마다 루프는 값 매개변수 팩의 각 요소를 로컬 변수에 바인딩합니다.

    이는 이 경우 i번째 반복에서 lhs의 i번째 요소가 로컬 변수 left에 바인딩된다는 것을 의미합니다.

    루프 본문에서는 로컬 변수를 평소처럼 사용할 수 있습니다.

    우리의 경우 각 요소 쌍을 비교하고 익숙한 guard 문을 사용하여 left != right인 쌍을 찾으면 false를 반환합니다.

     

    그리고 물론 이전처럼 에러를 던질 필요가 없습니다! 🎉


    Pack Iteration 활용하기

    이제 몇 가지 예제를 통해 Swift 코드에서 pack iteration을 활용할 수 있는 더 많은 방법을 살펴보겠습니다.

    모든 배열이 비어있는지 확인

    주어진 값 매개변수 팩의 모든 배열이 비어있는지 확인하는 함수를 작성해야 하는 상황을 생각해봅시다.

    func allEmpty<each T>(_ array: repeat [each T]) -> Bool {
        for a in repeat each array {
            guard a.isEmpty else { return false }
        }
        return true
    }
    

     

    위 함수는 타입 매개변수 팩 each T에 대해 제네릭이며, 값 매개변수 팩 array를 받습니다.

    이 타입은 repeat [each T] 팩 확장을 사용하여 선언되며, 여기서 [each T]가 반복 패턴입니다.

    호출 시점에서 대체된 팩의 각 요소에 대해 반복되어 값이 배열 리터럴 목록으로 확장됩니다.

    for-in repeat 루프의 각 반복에서 값 매개변수 팩 array의 요소가 로컬 변수 a에 바인딩됩니다.

    pack iteration에서는 값 팩의 요소가 필요에 따라 판단됩니다.

    즉, 값 팩의 모든 배열을 검사하지 않고도 함수에서 조기에 반환할 수 있습니다.

    이 경우 guard 문을 활용합니다.

     

    사용 예시

    print(allEmpty(["One", "Two"], [1], [true, false], []))
    // false
    

     


    ValueProducer 프로토콜 활용

    이제 pack iteration으로 크게 단순화되는 parameter packs의 사용 예제를 살펴보겠습니다.

     

    먼저 다음 프로토콜을 선언합니다.

     

    protocol ValueProducer {
        associatedtype Value: Codable
        func evaluate() -> Value
    }
    

     

    위 프로토콜 ValueProducer는 반환 타입이 Codable 프로토콜을 준수하는 연관 타입 Value인 evaluate() 메서드를 요구합니다.

    Result<ValueProducer, Error> 타입의 값으로 구성된 매개변수 팩을 받고, success 요소만 반복하며 그 값에 대해 evaluate() 메서드를 호출해야 한다고 가정해봅시다.

    또한 각 호출의 결과를 배열에 저장해야 합니다.

     

    pack iteration은 이 작업을 매우 쉽게 만듭니다!

    func evaluateAll<each V: ValueProducer, each E: Error>(result: repeat Result<each V, each E>) -> [any Codable] {
        var evaluated: [any Codable] = []
        for case .success(let valueProducer) in repeat each result {
            evaluated.append(valueProducer.evaluate())
        }
        return evaluated
    }
    

     

    먼저 evaluateAll 함수의 시그니처를 살펴봅시다.

    제네릭 매개변수 목록에서 두 개의 타입 매개변수 팩을 선언합니다.

    모든 요소는 위에서 선언한 프로토콜 ValueProducer를 준수해야 하고, 팩 each E의 모든 요소는 Error 프로토콜을 준수해야 합니다.

     

    함수는 팩 확장 타입 repeat Result<each V, each E>를 가진 단일 인자 result를 받습니다.

    이는 패턴 Result<each V, each E>가 런타임에 팩 each V와 each E의 모든 요소에 대해 반복된다는 것을 의미합니다.

    함수 본문을 구현하기 위해 먼저 evaluated 배열을 초기화합니다.

    다음으로 for case 패턴을 사용하여 Result 열거형의 success 케이스에 대해서만 루프 본문을 실행할 수 있어요.

     

    ValueProducer 타입의 값을 포함할 valueProducer 변수를 가져올 수 있습니다.

    이제 evaluate() 메서드 호출의 결과를 evaluated 배열에 추가하고, 최종적으로 반환합니다.

     

    사용 예시

    struct IntProducer: ValueProducer {
        let contained: Int
        
        init(_ contained: Int) {
            self.contained = contained
        }
        
        func evaluate() -> Int {
            return self.contained
        }
    }
    
    struct BoolProducer: ValueProducer {
        let contained: Bool
        
        init(_ contained: Bool) {
            self.contained = contained
        }
        
        func evaluate() -> Bool {
            return self.contained
        }
    }
    
    struct SomeError: Error {}
    
    print(evaluateAll(result:
        Result<IntProducer, SomeError>.success(IntProducer(5)),
        Result<IntProducer, SomeError>.failure(SomeError()),
        Result<BoolProducer, SomeError>.success(BoolProducer(true))))
    // [5, true]
    

     


    실제 활용 시나리오

    Pack Iteration이 실제로 어떻게 유용할지 몇 가지 시나리오를 생각해봤습니다.

     

    다양한 타입의 로깅

    protocol Loggable {
        func formatForLog() -> String
    }
    
    extension Int: Loggable {
        func formatForLog() -> String { "\(self)" }
    }
    
    extension String: Loggable {
        func formatForLog() -> String { "\"\(self)\"" }
    }
    
    extension Bool: Loggable {
        func formatForLog() -> String { self ? "true" : "false" }
    }
    
    func logAll<each T: Loggable>(_ items: repeat each T) {
        for item in repeat each items {
            print("LOG: \(item.formatForLog())")
        }
    }
    
    // 사용 예시
    logAll(42, "Hello", true, 3.14)
    // LOG: 42
    // LOG: "Hello"
    // LOG: true
    // LOG: 3.14
    

     

    여러 비동기 작업 처리

    func executeAll<each T>(
        _ operations: repeat () async throws -> each T
    ) async throws -> (repeat each T) {
        var results: (repeat each T)
        
        for operation in repeat each operations {
            // 각 작업을 순차적으로 실행
            let result = try await operation()
            // 결과를 튜플에 저장
            // (실제 구현은 더 복잡하지만 개념적으로 이런 식입니다)
        }
        
        return results
    }
    

     

    유효성 검사

    protocol Validator {
        associatedtype Value
        func validate(_ value: Value) -> Bool
        var errorMessage: String { get }
    }
    
    func validateAll<each V: Validator>(
        values: repeat (each V).Value,
        validators: repeat each V
    ) -> [String] {
        var errors: [String] = []
        
        for (value, validator) in repeat (each values, each validators) {
            if !validator.validate(value) {
                errors.append(validator.errorMessage)
            }
        }
        
        return errors
    }
    
    // 사용 예시
    struct EmailValidator: Validator {
        let errorMessage = "Invalid email format"
        func validate(_ value: String) -> Bool {
            value.contains("@")
        }
    }
    
    struct AgeValidator: Validator {
        let errorMessage = "Age must be positive"
        func validate(_ value: Int) -> Bool {
            value > 0
        }
    }
    
    let errors = validateAll(
        values: "invalid-email", -5,
        validators: EmailValidator(), AgeValidator()
    )
    print(errors)
    // ["Invalid email format", "Age must be positive"]
    

     


    Parameter Packs vs 기존 방식 비교

    Pack Iteration의 장점을 더 명확히 이해하기 위해 기존 방식과 비교해볼까요?

    기존 방식: 오버로딩

    // 2개 인자
    func process<T1, T2>(_ v1: T1, _ v2: T2) where T1: Equatable, T2: Equatable {
        if v1 as? T2 == v2 as? T1 { print("Equal") }
    }
    
    // 3개 인자
    func process<T1, T2, T3>(_ v1: T1, _ v2: T2, _ v3: T3) where T1: Equatable, T2: Equatable, T3: Equatable {
        // ...
    }
    
    // 4개, 5개... 계속 추가해야 함 😫
    

     

    Parameter Packs 방식

    func process<each T: Equatable>(_ values: repeat each T) {
        for value in repeat each values {
            print("Processing: \(value)")
        }
    }
    
    // 임의의 개수 인자 지원! 🎉
    process(1, "hello", true, 3.14, [1, 2, 3])
    

     

    성능 비교

    // 기존: 런타임에 타입 체크와 캐스팅 필요
    func oldWay<T1, T2>(v1: T1, v2: T2) {
        // 타입 체크 오버헤드
        if let v1AsT2 = v1 as? T2 {
            // ...
        }
    }
    
    // Parameter Packs: 컴파일 타임에 타입 안전성 보장
    func newWay<each T: Equatable>(_ values: repeat each T) {
        // 타입이 컴파일 타임에 확정되어 더 빠름
        for value in repeat each values {
            // 타입 캐스팅 불필요
        }
    }
    

     


    주의할 부분

    Pack Iteration을 사용할 때 알아야 할 몇 가지 제약사항이 있습니다.

    1. 동일한 길이의 팩만 함께 반복 가능

    func pairUp<each T, each U>(
        _ first: repeat each T,
        _ second: repeat each U
    ) {
        // ❌ 컴파일 에러: 두 팩의 길이가 같다는 보장이 없음
        for (f, s) in repeat (each first, each second) {
            print("\(f), \(s)")
        }
    }
    
    // ✅ 같은 팩 또는 타입 시스템이 길이를 보장하는 경우만 가능
    func pairUpSafe<each T>(
        _ first: repeat each T,
        _ second: repeat each T  // 같은 타입 매개변수 팩
    ) {
        for (f, s) in repeat (each first, each second) {
            print("\(f), \(s)")
        }
    }
    

     

    2. 중첩된 팩 확장의 제약

    func nested<each T, each U>(
        _ outer: repeat each T,
        _ inner: repeat each U
    ) {
        // ❌ 중첩된 팩 반복은 아직 지원하지 않음
        for o in repeat each outer {
            for i in repeat each inner {
                // ...
            }
        }
    }
    

     

    3. 인덱스 접근 불가

    func indexed<each T>(_ values: repeat each T) {
        for value in repeat each values {
            // ❌ 현재 인덱스를 직접 얻을 수 없음
            // let index = ??? 
        }
    }
    
    // 해결 방법: enumerated 같은 헬퍼 함수 구현 필요
    

     


    Conclusion

    핵심을 정리해볼까요?

    Pack Iteration의 장점

    • 🎯 직관적인 for-in 구문 사용
    • 🚀 임의 개수의 제네릭 인자 처리
    • ✅ 조기 반환으로 효율적인 검사
    • 💪 타입 안전성 보장
    • 🧹 보일러플레이트 코드 제거

    주요 사용 케이스

    • 가변 개수의 인자를 받는 제네릭 함수
    • 여러 타입의 값을 동시에 처리
    • 조건부 검사와 유효성 검증
    • Result 타입의 팩에서 선택적 처리

    주의사항

    • 같은 길이의 팩만 함께 반복 가능
    • 중첩 반복은 아직 제한적
    • 인덱스 접근은 직접 지원하지 않음

    개인적으로 Pack Iteration은 Swift의 제네릭 시스템을 한 단계 더 발전시킨 기능이라고 생각합니다.

    이전에는 복잡한 에러 처리와 로컬 함수를 동원해야 했던 작업이 이제는 익숙한 for-in 루프로 간단하게 처리됩니다.

    특히 Swift 5.9의 Parameter Packs가 가능성을 열었다면, Swift 6.0의 Pack Iteration은 그것을 실용적으로 만든 게임 체인저라고 볼 수 있습니다.

    6개 요소 튜플 제한 같은 인위적인 제약에서 벗어나 진정한 제네릭 프로그래밍의 자유를 얻게 된 것 같습니다! 🎉

    여러분은 어떤 상황에서 Pack Iteration을 활용할 수 있을 것 같나요? 🤔

     


    References

     

    Iterate Over Parameter Packs in Swift 6.0

    Parameter packs, introduced in Swift 5.9, make it possible to write generics that abstract over the number of arguments. This eliminates the need to have overloaded copies of the same generic function for one argument, two arguments, three arguments, and s

    www.swift.org

     

    swift-evolution/proposals/0408-pack-iteration.md at main · swiftlang/swift-evolution

    This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution

    github.com

    'Swift' 카테고리의 다른 글

    Swift Build Technologies  (0) 2025.11.01
    Swift SDK for Android - Android 앱을 Swift로 개발하기  (0) 2025.10.26
    Swift Profile Recorder  (0) 2025.10.12
    Swift 6.2  (0) 2025.10.02
    Deep Dive @_silgen_name  (1) 2025.09.20
Designed by Tistory.