Primary associated type

Park Jong Ho·2023년 1월 7일
0

Swift Primary Associated Type

Primary Associated type은 Swift 5.7 버전에서 새로 나온 기능으로, 연관 타입이 존재하는 프로토콜과 관련된 코드를 작성할 때 훨씬 가독성 높은 코드를 작성할 수 있도록 추가된 기능이다.

그렇다면 Primary Associated type은 무엇일까? Primary Associated type에 대해 알아보기 전에, 우선 연관타입이 있는 프로토콜 타입을 사용하는 예시를 보도록 하자.

일단 첫 번째 예시부터

func someFunction(_ collection: Collection) {
 //...
}

위 코드는 컴파일이 안된다. 그 이유는 무엇일까?

우선 Collection은 프로토콜으로, 다음과 같이 여러 개의 연관 타입이 존재한다.

public protocol Collection {
  associatedtype Element
  associatedtype Index
	associatedtype SubSequence
}

someFunction과 같이 프로토콜을 first type (매개변수나, 어떤 지역변수 또는 프로퍼티의 타입으로 사용한다는 뜻) 으로 사용한다는 것은 Exsitential type으로 사용하겠다는 뜻과 같다.

그 말인 즉슨, 아래와 같이 프로토콜을 타입 그 자체로 사용하면 런타임에 해당 프로토콜을 준수하는 그 어떤 구체 타입의 인스턴스라도 런타임에 filter 프로퍼티를 대체할 수 있다는 뜻이다. 그러면 filter 프로퍼티의 함수는 dynamic dispatch 방식으로 호출될 것이다.

protocol Filter {
  func filter()
}

final class SomeClass {
  var filter: Filter // Filter 프로토콜을 타입 그 자체로 사용한다! (First type)
  
  init(_ filter: Filter) {
    self.filter = filter
  }

}

let someClass: SomeClass = SomeClass(SoemFilter())
someClass.filter = anotherFilter() // filter 프로퍼티는 Filter 프로토콜을 준수하는 어떤 구체 타입으로도 대체할 수 있다. 그것도 런타임에

어쨌든 프로토콜이 First type으로 사용될려면, 연관 타입이 존재하면 안 된다. 왜냐하면 연관 타입은 구체 타입에 의해 결정되기 때문에, 프로토콜 타입만으로는 해당 연관타입들이 어떤 구체타입으로 대체될 지 컴파일러가 알 수 없기 때문이다.

func someFunction(_ collection: Collection) {
 //...
}

즉 위 함수의 Collection 프로토콜 타입은 연관 타입에 대한 정보를 제공해 줄 수 없기 때문에, 매개변수의 타입으로 사용할 수 없는 것이다.

다만 우리는 연관타입을 가지는 프로토콜에 관련된 코드를 작성할 경우 항상 아래와 같이 Generic Constraint로 타입을 작성하곤 했었다.

func someFunction<SomeCollection: Collection>(_ collection: SomeCollection) {
  
}

위와 같이 함수를 작성할 경우, SomeCollection은 Collection을 준수하는 어떤 구체 타입에 대한 파라미터가 되고, 매개변수인 collection은 항상 Collection을 준수하는 어떤 구체 타입이 올 것임을 컴파일러가 확신할 수 있기 때문에, 정상적으로 컴파일 될 수 있는 것이다.

또한 위와 같이 작성된 함수를 사용하는 아래와 같은 코드가 존재한다고 가정해보자.

func someFunction<SomeCollection: Collection>(_ collection: SomeCollection) {
  //...
}

someFunction([1,2,3,4,5])
someFunction(["Hello", "World", "Hello", "Swift"])

someFunction을 Int 타입의 Collection, 또 String 타입의 Collection으로 호출하는 것을 볼 수 있다.
이러한 호출부에 의해 someFunction은 Generic specialization 과정을 거쳐 static한 function으로 변환되게 된다.

func someFunctionInt(_ collection: Array<Int>) {
  //...
}

func someFunctionString(_ collection: Array<String>) {
  //...
}

즉 위와 같은 함수는 Generic에 의해 더 이상 dynamic한 방식으로 호출되지 않고, static한 방식으로 호출되기 때문에 static dispatch의 장점을 그대로 가져갈 수 있다.

런타임에 구체 타입을 검사하지 않아도 되기 때문에 함수의 구현부분을 컴파일러가 바로 볼 수 있고, 이것을 인라이닝 할 수 있어 불필요한 함수 호출을 줄일 수 있다.

Swift 5.7의 some

위에서 연관 타입이 존재하는 Collection을 매개변수로 사용하기 위해 Generic constraint 형식으로 지정할 수 있다는 것을 보았다. Swift 5.7은 매개변수에 some을 사용할 수 있도록 개선되었는데, someFunction을 다음과 같이 작성할 수도 있다.

func someFunction(_ collection: some Collection) {
  
}

더 이상 generic constraint으로 지정하지 않고도, some 키워드를 통해 Collection 프로토콜을 준수하는 어떤 구체 타입이 매개변수로 넘어올 것임을 컴파일러가 알 수 있다. 위 코드는 Generic constraint로 작성한 코드와 완전히 동일한 기능을 수행한다.

그러나 여전히 한 가지 불편한 점이 존재한다. 그것은 Collection의 연관 타입인 Element를 알 수 없다는 것이다. Collection의 Element 타입은 String이 될 수도, Int가 될 수도, 아니면 우리가 새로 정의한 Custom type이 될 수도 있다. 그러나 문제는 우리가 someFunction에서 Element가 도대체 어떤 타입인지 알 수 없다는 것이다.

Generic constraint를 사용하면, 적어도 where절을 사용해서 특정 Element 타입에 대응하는 함수를 작성할 수 있다.

// Int를 Element 타입으로만 가지는 Collection을 위한 함수
func someFunction<SomeCollection: Collection>(_ collection: SomeCollection) where SomeCollection.Element == Int { 
}

위와 같이 Generic constraint를 사용하면, 이제 Element가 Int 타입인 Collection 해당 함수의 매개변수로 넘길 수 있다!

즉 이제 Collection의 연관 타입을 정확히 알 수 있다는 뜻이다.

Primary Associated type을 사용해보자

위와 같이 Generic constraint를 사용해서도 우리가 원하는 기능을 얻을 수 있지만, Generic constraint는 코드가 너무 길어지는 경향이 있다. (angle brackets부터, where 절까지...)

이 같은 상황을 개선하기 위해 Swift 5.7에선 Protocol에 Primary Associated type을 명시할 수 있다. Primary associated type은 Generic constraint와 동일하게 동작하는데, 지금부터 사용방법을 한 번 보도록 하자.

Primary associated type은 다음과 같이 선언할 수 있다.

protocol Loadable<Value> {
    associatedtype Value

    func load() async throws -> Value
}

연관 타입으로 선언된 Value가 Loadable 선언 바로 옆 <> 사이에도 들어간 것을 볼 수 있다.

위와 같이 프라이머리 타입을 지정해주면, 우리는 위 프로토콜 타입에 관련된 함수를 작성할 때 다음과 같이 작성할 수 있다!

// Primary Associated type을 통해서, Value 연관 타입이 String이라는 것을 명시해줄 수 있다!
func someFunction(_ loadable: some Loadable<String>) {
  //...
}

또한 위의 함수는 아래 방식처럼 작성한 코드와 완전히 동일하다.

func someFunction<StringLoadable: Loadable>(_ loadable: StringLoadable) where StringLoadable.Value == String {
}

Generic constraint 방식과, Primary associated type을 비교하면 어떤 것이 더 읽기 쉬운 코드인지 한 눈에 들어올 것이다! 또한 Primary Associated type은 다음과 같이 extension에서도 명시할 수 있다.

만약 우리가 String을 Value타입으로 가지는 Loadable에 대한 extension을 작성하고 싶다면, 다음과 같이 쉽게 작성할 수 있다.

extension Loadable<String> {
    func load() async throws -> Value {
        return "Hello world"
    }
}

// 위 코드는 아래 코드와 완전히 동일하다!
extension Loadable where Value == String {
    func load() async throws -> Value {
        return "Hello World"
    }
}

앞으로 Primary 연관 타입을 사용해, Generic constraint을 활용한 복잡한 코드 대신 간결하게 코드를 작성해보도록 하자

References

https://www.donnywals.com/what-are-primary-associated-types-in-swift-5-7/

https://www.swiftbysundell.com/articles/opaque-return-types-primary-associated-types/

profile
iOS 개발자입니다.

0개의 댓글