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의 장점을 그대로 가져갈 수 있다.
런타임에 구체 타입을 검사하지 않아도 되기 때문에 함수의 구현부분을 컴파일러가 바로 볼 수 있고, 이것을 인라이닝 할 수 있어 불필요한 함수 호출을 줄일 수 있다.
위에서 연관 타입이 존재하는 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의 연관 타입을 정확히 알 수 있다는 뜻이다.
위와 같이 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을 활용한 복잡한 코드 대신 간결하게 코드를 작성해보도록 하자
https://www.donnywals.com/what-are-primary-associated-types-in-swift-5-7/
https://www.swiftbysundell.com/articles/opaque-return-types-primary-associated-types/