Understand type eraure:
associated type이 있는 protocol이 existential any type과 어떻게 상호 작용하는지 설명.
Hide implementation details:
interface와 implementation을 분리해서 캡슐화하기 위해 opaque type을 사용하는 방법에 대해 설명.
Identify type releationships:
protocol에서 same-type requirements가 concrete type의 여러가지 다른 sets간의 관계를 모델링하는 방법에 대해 설명.
Understand type eraure
protocol Animal {
associatedtype CommodityType: Food
func produce() -> CommodityType
}
struct Chicken: Animal {
func produce() -> Egg { ... }
}
struct Cow: Animal {
func produce() -> Milk { ... }
}
protocol Food { ... }
struct Egg: Food { ... }
struct Milk: Food { ... }
1. 음식의 생산을 추상화하기 위해 Animal protoco에 produce() 메소드 추가.
2. concrete Animal type이 주어지고, produce()를 호출하면, concrete Animal type에 의존하는 특정 type의 Food가 반환된다.
3. protocol ‘Self’ type은 ‘Animal’ protocol을 준수하는 실제 concrete type을 나타낸다.
4. ‘Self’ type에는 ‘Food’를 준수하는 associated ‘Commodity’ type이 있다.
5. Chicken과 Cow는 CommodityType이 ‘Egg’, ‘Milk’로 Animal protocol을 준수하고 있다.
struct Farm {
var animals: [any Animal]
func produceCommodities() -> [any Food] {
return animals.map { animal in
animal.produce()
}
}
}
6. Farm의 ‘animals’ 프로퍼티는 ‘any Animal’의 여러 다른 종류로 이루어진 배열이다.
서로 다른 concrete types에 대해 동일한 표현을 사용하는 것을 type erasure이라고 한다.
7. produceCommodities() 함수는 동물의 배열에 대해 map을 사용해 각각에 대한 produce()를 호출해 Food를 반환한다.
이는 간단해 보이지만 type erasure가 animal의 underlying type의 static type relationships을 제거한다. (Chicken, Cow로 고정된 타입의 관계를 제거한다는 뜻 같음.)
8. map 클로져의 animal parameter는 ‘any Animal’ type을 가지고, produce()의 반환 type은 associated type이다.
9. existential type에서 associated type을 반환하는 함수를 호출하면, 컴파일러는 type erasure를 사용해서 호출의 result type을 결정한다.
10. type erasure는 이러한 associated types을 동등한 제약조건이 달린 existential types로 바꾼다.
11. concrete Animal type과 associated CommodityType간의 관계를 ‘any Animal’, ‘any Food’로 대체해서 지웠다.
‘any Food’ type은 associated CommodityType의 upper bound라고 한다.
Type erasure semantics
12. produce() 함수는 ‘any Animal’에서 호출되므로 반환 값은 type이 지워져(erased) ‘any Food’ type의 값을 을 제공한다.
Swift5.7의 associated-type erasure의 동작에 대해서 살펴보면, protocol(func produce() -> CommodityType) 함수의 return type으로 나타나는 associated type은 함수를 호출하면 이 type의 값을 생성하기 때문에 ‘producing position’에 있다고 한다.
14. ‘any animal’에서 이 함수를 호출할 때 컴파일 시간에 concrete type을 알지 못하지만 그것이 upper bound의 subtype이라는 것은 알고 있다.
let animals: [any Animal] = [Cow()]
animals.map { animal in
animal.produce()
}
- 해당 예에서는 런타임에서 Cow를 보유하고 있는 ‘any Animal’에 대해 produce()를 호출하고 있다.
- Cow의 produce()는 Milk를 반환한다.
Milk는 Animal protocol의 associated Commodity Type의 upper bound인 ‘any Food’ 안에 저장될 수 있다. 이는 Animal protocol을 준수하는 모든 concrete type에 대해서 항상 안전하다.
그러면 associated type이 init 또는 parameter에 나타나면 어떻게 될까?
protocol Animal {
associatedtype FeedType: AnimalFeed
func eat(_: FeedType)
}
let animals = [Cow()]
animals.map { animal in
animal.eat(???)
}
- Animal protocol의 eat() 함수는 parameter(영상에서는 consuming position이라고 표현함)로 FeedType을 가진다.
- 변환이 반대 방향으로 진행되기 때문에 type erasure를 수행할 수 없다.
- concrete type을 알 수 없기 때문에 associated type의 upper bound type은 existential type의 concrete type으로 안전하게 변환되지 않는다.
- type erasure는 consuming position에서 associated type으로 적업하는것을 허용하지 않는다.
- 대신 opaque ‘some’ type을 사용하는 함수에 전달해서 existential ‘any’ type을 unbox(concrete tpye으로 푸는 것을 의미하는 듯 함.)해야 한다.
associated type이 있는 이 type erasure는 실제로 Swift5.6에서 볼 수 있는 기존 언어의 기능과 비슷하다.
protocol Cloneable: AnyObject {
func clone() -> Self
}
let object: any Cloneable = ...
let cloned = object.clone()
- 이 protocol은 Self를 반환하는 단일 clone() 함수를 정의한다.
- ‘any Cloneable’ 유형의 값에 대해 clone()을 호출하면 result type ‘Self’가 upper bound까지 type이 지워진다.
- Self type의 upper bound는 항상 protocol 자체이므로 ‘any Cloneable’ type의 새로운 값을 반환한다.
즉, ‘any’를 사용해서 값의 type이 protocol을 준수하는 some concrete type을 저장하는 exisential type임을 선언할 수 있다.
💡 이는 associated type이 있는 protocol에서도 동작한다.
producing position에서 associated type이 있는 protocol 함수를 호출할 때 associated type은 associated type의 제약 조건을 전달하는 또 다른 existential type인 upper bound 까지 type-erased된다.
Hide implementation details
동물에게 먹이를 줄 수 있도록 Animal protocol을 generalize 해보자.
protocol Animal {
var isHungry: Bool { get }
}
extension Farm {
//var hungryAnimals: [any Animal] {
// animals.filter(\\.isHungry)
//}
//var hungryAnimals: LazyFilterSequence<[any Animal]> {
var hungryAnimals: some Collection<any Animal> {
animals.lazy.filter(\\.isHungry)
}
func feedAnimals() {
for animal in hungryAnimals {
...
}
}
}
1. 배고픈 동물을 filter로 걸러내는 계산을 hungryAnimals이라는 computed property로 분리했다.
2. ‘any Animal’의 배열에서 filter()를 호출하면 새로운 ‘any Animal’ 배열이 반환된다.
3. 그러므로 feedAnimals()는 hungryAnimals의 결과를 한 번만 반복하고 임시 배열로 생성된 것을 버린다는 것을 알 수 있다.
Farm에 배고픈 동물이 많으면 비효율적.
4. 임시 할당을 피하려면 standard library의 lazy collections 기능을 사용할 수 있다.
5. ‘filter’에 대한 호출을 ‘lazy.filter’로 바꾸면 lazy collection이라고 알려진 것을 얻을 수 있다.
6. lazy collection은 ‘filter’에 대한 일반 호출로 반환된 배열과 동일한 요소를 갖지만 임시 할당을 피할 수 있다.
7. 그러나 이제 ‘hungryAnimals’ property의 type은 복잡한 concrete type인 ‘LazyFilterSequence of Array of any Animal’로 선언되야 한다. 이로 인해 불필요한 구현 세부 정보가 노출된다.
8. client feedAnimals()는 우리가 ‘hungryAnimals’ 구현에서 ‘lazy.filter’를 사용한 것에 대해 관심이 없다. 반복할 수 있는 collection을 가져오고 있다는 것만 알면 된다.
9. opaque result type을 사용해서 collection의 추상 interface 뒤에 복잡한 concrete type을 숨길 수 있다.
이제 ‘hungryAnimals’를 호출하는 client는 Collection protocol을 준수하는 concrete type을 얻고 있다는 것을 알지만, 특정 concrete type의 collection은 알지 못한다.
10. 따라서 Animal의 어떠한 함수도 호출할 수 없다.
some Collection<any Animal>
제한된 opaque result types은 Swift5.7의 새로운 기능이다. 제한된 opaque result type은 protocol 이름 뒤에 꺽쇠 안에 작성하면 된다.
11. client에게 LazyFilterSequence라는 사실이 숨겨지지만, 그게 Collection을 준수하는 어떤 concrete type이라는 것을 알고 있으며, 그 Element associated type은 ‘any Animal’과 같다.
protocol Collection<Element>: Sequence {
associatedtype Element
...
}
이것은 Collection protocol의 associated type이 primary associated type을 선언하기 때문에 정상적으로 동작한다.
Swift5.7 이전에는 특정 generic argument가 있는 existential type을 나타내기 위해 고유한 데이터 type을 써야 했다. 하지만 Swift5.7에는 제한된 existential types을 사용해 이 개념을 언어에 구축했다.
extension Fram {
var hungryAnimals: any Collection<any Animal> {
if isLazy {
return animals.lazy.filter(\\.isHungry)
} else {
return animals.filter(\\.isHungry)
}
}
}
코드가 두 가지 underlying type을 반환해야 할 경우 some 키워드의 사용이 불가능하고, any 키워드를 사용하면 가능하게 된다.
Collection과 같은 다양한 표준 라이브러리 protocol과 함께 사용할 수 있고, primary associated type을 갖도록 자신의 protocol을 선언할 수도 있다.
Identify type relationships
동물에게 먹이를 주기 전에 적절한 유형의 작물을 재배해고 작물을 수확해서 사료를 생산해야 한다면?
struct Cow: Animal {
func eat(_: Hay) { ... }
}
struct Hay: AnimalFeed {
static func grow() -> Alfalfa { ... }
}
struct Alfalfa: Crop {
func harvest() -> Hay { ... }
}
let cow: Cow = ...
let alfalfa = Hay.grow()
let hay = alfalfa.harvest()
cow.eat(hay)
struct Chicken: Animal {
func eat(_: Scratch) { ... }
}
struct Scratch: AnimalFeed {
static func grow() -> Millet { ... }
}
struct Millet: Crop {
func harvest() -> Scratch { ... }
}
let chicken: Chicken = ...
let millet = Scratch.grow()
let scratch = millet.harvest()
chicken.eat(scratch)
protocol AnimalFeed {
associatedtype CropType: Crop
where CropType.FeedType == Self
static func grow() -> CropType
}
protocol Crop {
associatedtype FeedType: AnimalFeed
where FeedType.CropType == Self
func harvest() -> FeedType
}
extension Farm {
func feedAnimals() {
for animal in hungryAnimals {
feedAnimals(animal)
}
}
private func feedAnimal(_ animal: some Animal) {
let crop = type(of: animal).FeedType.grow()
let feed = crop.harvest()
animal.eat(feed)
}
}
이 두 세트의 관련 concrete type을 추상화해서 feedAnimal() 함수를 한 번 구현하고, 이 함수를 Cow, Chicken 모두에게 먹일 뿐만 아니라 나중에 입양할 수 있는 새로운 type의 Animal도 먹일 수 있다.
- feedAnimal()은 consuming position(parameter)에 associated type이 있는 Animal protocol의 eat() 함수와 함께 동작해야 하므로 feedAnimal() 함수가 ‘some Animal’을 parameter로 받음.
- 만일 Alfalfa가 Hey가 아닌 Scratch를 반환한다고 코드를 잘못 작성했을때 우리는 이것을 알 방법이 없다.
- 하지만 where CropType.FeedType == Self, where FeedType.CropType == Self 하지만 해당 코드를 작성해 타입을 제한한다면 AnimalFeed와 Crop이 서로 무엇을 반환하고 있는지 알게 되므로 동물이 올바른 사료를 먹고 있다는 것이 보장된다.
마지막에 집중력이 떨어져서 요약을 많이 해버렸다..
'Swift > 학습' 카테고리의 다른 글
KeyPath (0) | 2023.06.13 |
---|---|
Existential any in Swift explained with code examples(번역) (0) | 2023.04.06 |
Swift Opaque Types(번역) (0) | 2023.03.28 |
enum으로 특정 단위를 명확하게 표현하기 (0) | 2022.09.25 |
2022 WWDC Embrace Swift Generics (1) | 2022.07.04 |