Generic 모델링에서 시작합니다.
아래와 같은 Animal
프로토콜이 있을 때,
이를 사용하는 Farm
struct의 메서드에서 Animal
의 Concrete 타입
으로 대체될 타입파라미터
적합성을 따르도록 적용할 수 있습니다.
protocol Animal {
associatedtype Feed: AnimalFeed
func eat(_ food: Feed)
}
struct Farm {
func feed<A: Animal>(_ animal: A) {
// ...
}
}
혹은 아래처럼 후행 where
절에서 프로토콜 적합성을 지정할 수 있습니다. where
절은 디테일하게 requirement와 타입 관계를 작성할 수 있게 해줍니다.
struct Farm {
func feed<A>(_ animal: A) where A: Animal {
// ...
}
}
where
절은 강력하지만 일반적인 함수에서는 복잡해보일 수 있는데
→ some
으로 간단하게 표현이 가능합니다.
some으로 프로토콜 적합성의 추상 타입을 표현
(⚠️ Swift 5.7 이전에는 프로퍼티 타입, 리턴 타입으로만 사용 가능)Swift 5.7
부터는 위와 같은 표현 대신 아래처럼 some
을 통해 프로토콜 적합성의 추상 타입으로 표현 가능합니다.
struct Farm {
func feed(_ animal: some Animal) {
// ...
}
}
where
절이 사라지고, 타입 파라미터
리스트도 사라져서 구문상 간결하며, 매개변수 선언에 Animal
매개변수에 대한 Semantics를 포함하기 때문에 더 간단합니다.
💡 여기에서 some은 작업중인 특정 타입이 있음을 나타냄. some 뒤에는 항상 적합성 requirement가 붙음. some 키워드는 매개변수와 결과 타입에도 사용될 수 있음.
기존에 SwiftUI에서 some
View
를 반환하는 것이 결과 타입에 사용되는 케이스입니다.
SwiftUI에서 View
에서 body
프로퍼티는 특정 타입의 View
를 반환하지만, body
프로퍼티를 사용하는 코드에서는 특정 타입이 무엇인지 알 필요가 없기 때문입니다.
var body: some View { ... }
Opaque Type(불투명한 타입)과 Underlying Type(기초 타입)
Opaque 타입
: 특정Concrete 타입
에 대한 placeholder를 나타내는 추상 타입.Underlying 타입
: 대체되는Concrete 타입
.
Opaque 타입
의 값의 경우, Underlying 타입
은 값의 스코프에 대해 고정됩니다.
이 방식으로 값을 사용하는 Generic
코드는 값에 접근할 때마다 동일한 Underlying 타입
을 얻도록 보장됩니다.
some Animal
과 <T: Animal>
은 모두 Opaque 타입
을 선언한 것.Opaque 타입
은 input, output 모두에 사용될 수 있기 때문에,
매개변수, result 타입 모두에 선언할 수 있습니다.
Opaque 타입
의 위치는 ‘프로그램의 어느 부분이 추상 타입을 보는지 결정하고, 프로그램의 어느 부분이 Concrete 타입
을 결정하는지’를 결정합니다.
func getValue<T>(Parameter) -> Result
명명된 타입 파라미터
는 항상 input 쪽에서 선언되므로 호출하는 쪽에서 Underlying 타입
을 결정하고, 구현에서는 추상타입을 사용합니다.
func getValue(Parameter) -> Result
일반적으로, Opaque
파라미터나 result 타입에 대한 값을 제공하는 프로그램 부분은 Underlying 타입
을 결정하고, 값을 사용하는 프로그램 부분은 추상 타입을 봅니다.
some에 대한 Underlying Type 추론
Underlying 타입
은 값에서 추론되기 때문에, Underlying 타입
은 항상 값과 동일한 위치에서 제공됩니다.
let animal: some Animal = Horse()
지역 변수의 경우 Underlying 타입
은 할당 오른쪽의 값에서 유추됩니다.
이는 Opaque 타입
의 지역 변수가 항상 초기 값을 가져야 함을 의미.
제공하지 않으면 컴파일러에서 에러가 발생됩니다.
var animal: some Animal = Horse()
animal = Chicken() // Error!
Underlying 타입
은 변수 범위에 대해 고정되어야 하기 때문에 Underlying 타입
를 변경하려고 하면 에러가 발생됩니다.
func feed(_ animal: some Animal) { }
feed(Horse())
Opaque 타입
이 있는 매개변수의 경우 Underlying 타입
은 호출부의 인수 값에서 유추됩니다.
func feed(_ animal: some Animal) { }
feed(Horse())
feed(Chicken())
Underlying 타입
은 매개변수 스코프에 대해서만 수정하면 되므로 각 호출은 서로 다른 인수 타입을 제공할 수 있습니다.
func makeView(for farm: Farm) -> some View {
FarmView(farm)
}
Opaque result 타입
의 경우 Underlying 타입
은 구현의 return 값에서 추론됩니다.
Opaque result 타입
이 있는 메서드나 연산 프로퍼티
는 프로그램의 어느 곳에서나 호출할 수 있으므로 이 명명된 값의 스코프는 전역.
이는 Underlying return 타입
이 모든 return 문에서 동일해야 함을 의미합니다.
func makeView(for farm: Farm) -> some View { // Error!!!
if condition {
return FarmView(farm)
} else {
return EmptyView()
}
}
그렇지 않은 경우 컴파일러는 Underlying return 값
의 타입이 일치하지 않는다는 에러를 발생시킵니다.
Opaque
SwiftUI의 View
는 ViewBuilder DSL(Domain-Specific Language)
은 제어흐름 문을 변환해서 각 분기에 대해 동일한 Underlying return 타입
을 갖도록 해줍니다.
@ViewBuilder
func makeView(for farm: Farm) -> some View {
if condition {
FarmView(farm)
} else {
EmptyView()
}
}
이런 경우 ViewBuilder
DSL로 문제를 해결 가능. 위처럼 메서드에 @ViewBuilder
annotation을 달고, return
문을 제거하면 ViewBuilder
타입으로 결과를 빌드할 수 있습니다.
다시 feed(_:Animal)
메서드로.
protocol Animal {
associatedtype Feed: AnimalFeed
func eat(_ food: Feed)
}
struct Farm {
func feed(_ animal: some Animal) { ... }
}
다른 곳에서는 Opaque 타입
을 참조할 필요가 없기 때문에 매개변수 리스트에서 some
을 사용할 수 있습니다.
protocol Animal {
associatedtype Feed: AnimalFeed
associatedtype Havitat
func eat(_ food: Feed)
}
struct Farm {
func feed(_ animal: some Animal) { ... }
func buildHome<A>(for: animal: A) -> A.Havitat where A: Animal { ... }
}
함수 signature에서 Opaque 타입
을 여러 번 참조해야 하는 경우 Name 타입 파라미터
가 유용.
예를 들어, 위처럼 Animal
프로토콜에 Habitat
(서식지)라는 또다른 associatedtype
을 추가하면 특정 동물을 위해 농장에 habitat를 build하기를 원할 수 있습니다.
이런 경우, 특정 Animal
의 종류에 따라 result 타입이 달라지기 때문에
매개변수 타입과 return 타입에 타입 파라미터
A
를 사용해야 합니다.
struct Silo<Material> {
private var storage: [Material]
init(storing materials: [Material]) {
self.storage = materials
}
}
var hayStorage: Sile<Hay> // Material을 참조하려면 명시적으로 타입파라미터를 지정
Opaque 타입
을 여러번 참조하는 일반적인 경우는 Generic 타입
.
코드는 종종 Generic 타입
에 대한 타입 파라미터
를 선언하고, 저장 프로퍼티
에 대한 타입 파라미터
를 사용하고, 다시 memberwise 이니셜라이저
에서 사용합니다.
다른 컨텍스트에서 Generic 타입
을 참조하려면 <>
안에 타입 파라미터
를 명시적으로 지정해야 합니다.
선언의 <>
는 Generic 타입
을 사용하는 방법을 명확히 하는 데 도움이 될 수 있으므로 Opaque
타입은 항상 Generic 타입
에 대해 이름을 지정해야 합니다.
다시, feed(_:)
메서드를 작성.
protocol Animal {
associatedtype Feed: AnimalFeed
func eat(_ food: Feed)
}
struct Farm {
func feed(_ animal: some Animal) {
let crop = type(of: animal).Feed.grow() // 1
let produce = crop.harvest() // 2
animal.eat(produce) // 3
}
}
Animal
매개변수의 타입을 사용하여 Feed
associatedtype
을 통해 성장할 작물 유형에 접근 가능합니다.
Feed.grow()
를 호출하여 이러한 타입의Feed
를 생성하는 작물(crop
)의 인스턴스를 가져옴.- 그리고 작물의
harvest()
를 호출해서 농작물을 수확. - 수확한 작물을 동물에게
eat()
Underlying
animal 타입이 고정되어 있기 때문에, 컴파일러는 여러 메서드 호출을 통해 식물 타입, animal 타입 간의 관계를 알 수 있습니다.
이런 정적 관계는 animal에게 잘못된 타입의 먹이를 주는 실수를 방지시켜줍니다.
이어서 모든 동물들에게 먹이를 주는 메서드를 작성.
protocol Animal {
associatedtype Feed: AnimalFeed
func eat(_ food: Feed)
}
struct Farm {
func feed(_ animal: some Animal) {
let crop = type(of: animal).Feed.grow()
let produce = crop.harvest()
animal.eat(produce)
}
func feedAll(_ animals: [some Animal]) {
}
}
element
타입이 Animal
프로토콜을 따라야한다는 것을 알지만, 배열이 다양한 타입의 Animal
을 저장할 수 있기를 원합니다.
some
에는 변경할 수 없는 특정 Underlying 타입
이 있음. Underlying 타입
이 고정되어 있기 때문에 배열의 모든 요소는 동일한 타입을 가져야 합니다.
하지만 우리가 원하는 것은 다양한 타입의 Animal
을 저장할 수 있는 배열인데 이럴 때 any 키워드가 나옵니다.
any 키워드 (exsistential 타입) & Type Erasure 전략
any
Animal
이라고 쓰면 임의의 종류의 animal을 표현할 수 있습니다.
any
키워드는 이 타입이 어떤(any) 임의 타입의 animal을 저장할 수 있으며, animal의 Underlying
타입이 런타임에 달라질 수 있음을 나타냅니다.
some
키워드와 마찬가지로 any
키워드 뒤에는 항상 requirement 적합성을 따릅니다.
any
Animal
은 구체적인 Animal
타입을 동적으로 저장할 수 있는 single static type(단일 정적 타입)
. 이를 통해 값 타입과 함께 Subtype 다형성
을 이용할 수 있습니다.
이런 유연한 저장을 허용하기 위해 모든 Animal 타입은 메모리에 특별한 표현(representation)을 가집니다.
이 표현은 상자로 비유됩니다.
어떤 값은 상자 안에 직접 들어갈 만큼 작을 수도 있고,
어떤 값은 상자에 비해 너무 커서 값을 다른 곳에 할당하고 상자 안에는 해당 값에 대한 포인터를 저장하게 됩니다.
구체적인 Animal
타입을 동적으로 저장할 수 있는 any
Animal
을 공식적으로는 existential 타입
(실존타입)이라고 부릅니다.
그리고 서로 다른 Concrete 타입
에 대해 동일한 표현을 사용하는 전략을 Type Erasure
라고 부릅니다.
Concrete 타입
은 컴파일 타임에 지워지고, Concrete 타입
은 런타임에만 알려집니다.
exsistential 타입
인 any
Animal
의 두 인스턴스는 동일한 정적 타입
을 갖지만 동적 타입
은 다릅니다.
Type Erasure
는 서로 다른 Animal 값 사이의 타입 레벨 특성
을 제거하여
서로 다른 동적 타입
의 값을 동일한 정적 타입
으로 교환하여 사용할 수 있게 해줍니다.
Type Erasure
를 사용해서 이질적인 값 타입 배열을 작성할 수 있게 되는데, 이를 이용해서 feedAll()
메서드를 작성할 수 있습니다.
protocol Animal {
associatedtype Feed: AnimalFeed
func eat(_ food: Feed)
}
struct Farm {
func feed(_ animal: some Animal) {
let crop = type(of: animal).Feed.grow()
let produce = crop.harvest()
animal.eat(produce)
}
func feedAll(_ animals: [any Animal]) {
for animal in animals {
animal.eat(???) // Error!!: Member 'eat' cannot be used on value of type 'any Animal'
}
}
}
매개변수 타입을 any
Animal
배열로 받도록 합니다.
배열의 각 animal들의 eat()
을 호출하려면 underfying
Animal
에 대한 특정 Feed
타입이 필요.
eat()
을 호출하자마자 컴파일러가 에러를 보내게 됨.(Member 'eat' cannot be used on value of type 'any Animal')
특정 Animal
타입 간의 타입 레벨 특성
을 제거했기 때문에 associatedtype
을 포함해서 특정 Animal
타입에 의존하는 모든 타입 관계도 제거됨. 따라서 우리는 이 animal이 어떤 종류의 먹이를 기대하는지 알 수가 없게 됩니다.
타입 관계에 의존하기 위해서는 특정 타입의 animal이 고정되어 있는 context로 다시 돌아가야 합니다.
animal에서 직접 eat()
을 호출하는 대신,some
Animal
을 허용하는 feed()
메서드를 호출해야 합니다.
any
Animal
은 some
Animal
과 다른 타입이지만,
컴파일러는 underfying 값
을 unboxing하고, 이를 some
Animal
매개변수에 직접 전달하여,any
Animal
의 인스턴스를 some
Animal
로 변환할 수 있습니다.
unboxing은 컴파일러가 상자를 열고 그 안에 저장된 값을 꺼내는 것으로 비유.
some Animal 매개변수의 scope에 대해, 값은 고정된 underfying 타입
을 가지므로 associatedtype
에 대한 접근을 포함한 underfying 타입
에 대한 모든 작업에 접근할 수 있습니다.
이는 필요할 때 유연한 저장소를 선택할 수 있게 해주면서,
함수 scope에 대한 underfying 타입
을 고정시켜 정적 타입 시스템
의 완전한 표현력을 갖는 context로 돌아갈 수 있게 만들어 줍니다.
그리고 대부분의 경우 unboxing에 대해 생각할 필요가 없습니다. 이는 Animal
에서 프로토콜 메서드를 호출하는 것이 실제로 underfying 타입
에서 메서드를 호출하는 방식과 유사하기 때문입니다.
protocol Animal {
associatedtype Feed: AnimalFeed
func eat(_ food: Feed)
}
struct Farm {
func feed(_ animal: some Animal) {
let crop = type(of: animal).Feed.grow()
let produce = crop.harvest()
animal.eat(produce)
}
func feedAll(_ animals: [any Animal]) {
for animal in animals {
feed(animal) // any Animal을 some Animal로 unboxing
}
}
}
위처럼 feed
로 animal(any
Animal
)을 전달할 수 있고,
각 반복마다 특정 Animal
에게 먹일 적절한 작물을 harvest하고 product시켜서 eat하도록 할 수 있습니다.
some과 any 정리
some
underfying 타입
이 고정됨.Generic
코드에 대한 타입 관계에 의존할 수 있으므로, 작업 중인 프로토콜의 API 및associatedtype
에 대한 전체 접근 권한을 가짐.
any
- 임의의
Concrete 타입
을 저장해야 하는 경우 사용. Type Erasure
를 제공 ⇒ 이질적인 컬렉션을 표현할 수 있고, optional을 사용해서underfying 타입
이 없음을 나타내고, 추상화를 구현 세부사항으로 만들 수 있음.
일반적으로,
기본적인 some
을 쓰고,
임의의 값을 저장해야 하는 상황일 때 some
대신 any
를 사용하면 됩니다.
이런 접근방식을 제공하게 되면,
제공되는 저장소의 유연성이 필요할 때만 Type Erasure
와 Semantics 제한
의 비용을 지불하게 됩니다.
(이 작업 흐름은 기본적으로 let을 쓰고, mutation이 필요할 때만 var를 쓰는 흐름과 유사)
Swift 6에서 변경 예정
(https://github.com/apple/swift-evolution/blob/main/proposals/0335-existential-any.md)
기존에는 많이 사용하는 아래와 같은 케이스에서 Existential Type
과 Concrete Type
의 선언방식이 동일하여 구분이 되지 않는 문제가 발생.
// Existential Type인지 Concrete Type인지 구분이 되지 않음
var animal: Animal
var cow: Cow
// Animal class를 상속받아야 하는 것인지, Animal Protocol을 confirm해야하는 것인지 구분이 되지 않음
func feed<A: Animal>(_ animal: A) { }
⚠️ Swift 6에서는 프로토콜 변수를 선언할 때 프로토콜 타입명 앞에 항상 any를 붙이도록 강제할 예정입니다.
var anyAnimal: any Animal = Cow()