그러기 위해선 위와 같이 Horse, Chicken 을 나타내는 구조체를 추가로 선언하고, 각 동물에게 먹일 음식들도 추가적으로 선언해야 한다.
또한 농장(Farm) 에서 모든 동물에게 먹이를 먹일 수 있는데, 이것을 코드로 나타내면 엄청난 양의 Boiler Plate 코드가 탄생한다.
이때 여러분이 반복적인 구현으로 오버로드를 작성하고 있다면 일반화하라는 신호일 수 있습니다라고 이야기한다
struct Farm {
func feed(_ animal: Cow) {
let alfalfa = Hay.grow()
let hay = alfalfa.harvest()
animal.eat(hay)
}
func feed(_ animal: Horse) {
let root = Carrot.grow()
let carrot = root.harvest()
animal.eat(carrot)
}
func feed(_ animal: Chicken) {
let wheat = Grain.grow()
let grain = wheat.harvest()
animal.eat(grain)
}
}
Identify common capabilities
말과 닭, 소와 같은 동물 타입의 구조체들을 선언했을 때, 동물들은 모두 어떤 음식을 먹을 수 있는 eat 메소드를 가지고 있는 것을 확인할 수 있었다.
각 동물들은 서로 다른 음식을 먹고, 또한 음식을 먹는 방법 또한 다를 것이다.
우리는 여기서 eat 메소드를 abstract code 로 작성할 수 있고, eat 메소드가 정의된 concrete type 에 따라 다르게 동작하도록 정의할 수 있다.
abstract code 가 서로 다른 concrete type 에서 다르게 동작하는 것을 polymorphism (다형성) 이라고 한다.
Polymorphism - 다형성
추상 코드가 먹기 메서드를 호출하도록 허용하고 추상 코드가 작동하는 구체적인 유형에 따라 다르게 동작하도록 구축하려고 한다. 다양한 구체적인 유형에 대해 추상 코드가 다르게 동작하는 능력을 ‘다형성’이라고 하고 다형성을 통해 코드 한 개는 사용되는 방식에 따라 여러 가지 동작을 가질 수 있다
중요한 부분은 다형성을 통해 코드 한 개는 사용되는 방식에 따라 여러 가지 동작을 가질 수 있습니다라는것이다
다형성의 다양한 형태
function overloading(함수 오버로딩)
polymorphism 이라고 불린다
argument type 에 따라 같은 메소드 호출이 다른 의미를 가질 때를 말한다.
일반적인 해결책은 아니다
Subtype(하위 유형 다형성)
Super Type 의 코드가 특정 subtype 에서 다른 동작을 할 때를 말한다.
Parametric(매개변수 다형성 - 제네릭 사용)
Generic 에 의해 형성되는 polymorphism
Generic code 는 타입 파라미터를 사용한다.
타입 파라미터를 통해 여러 가지 타입에서 사용되는 하나의 코드 덩어리를 작성할 수 있다.
concrete type 들은 제네릭 타입의 argument 로 사용될 수 있다.
Subtype polymorphism (서브타입 다형성) 을 이용해 위와 같은 문제를 해결해보자
Subtype 관계를 표현하는 방법으로 class hierarchy (클래스 계층구조) 가 있다.
Animal class 를 선언하고, 모든 동물이라면 기본적으로 가져야할 eat 메소드를 선언해보자
class Animal {
func eat(_ food: ???) {fatalError("Subclass must implement `eat`")}
}
위의 코드는 Animal이라는 클래스가 eat이라는 함수를 가지고있고 이를 상속받는 함수는 이 함수를 override할수있게된다 하지만 상속받는 클래스의 특징에 따라 food가 다르기 때문에 이부분을 해결해야한다는 문제를 가지고있다, 여기서는 구체적인 타입이 될수가 없구나 하는정도만 이해하고 우선은 이대로 넘어간다
그 다음 구조체로 선언된 각 동물들을 클래스 타입으로 바꾸고, Animal 슈퍼클래스를 상속시키자
이제 모든 동물 타입을 표현할 수 있는 abstract-base class 인 Animal 을 가지고 있다.
Animal type 의 eat 메소드를 호출하는 것은, subtype polymorphism 을 사용해 subclass implementation 을 호출할 것이다.
위와 같은 subtype polymorphism 의 단점
각 동물은 서로 다른 타입의 음식을 먹는다. 이것은 클래스 계층 구조로 표현하기 정말 어렵다
인스턴스 간 상태를 공유하고 싶지 않아도 강제로 Reference 타입인 class 타입을 사용해야 한다.
super class 의 method 를 재정의하지 않으면, 런타임 이전까지 에러를 알지 못한다. 왜냐면 class의 함수는 direct dispatch방식이 아니라 table dispatch방식이기때문에 런타임이 되서야 함수를 알 수 있게된다. 컴파일시점에서는 알수없기때문에 문제가 발생할 수 있다
각 동물이 서로 다른 타입의 음식을 먹는 것을 어떻게 표현할 수 있을까?
첫 번째 방법: Any 사용하기
class Animal {
func eat(_ food: Any) {fatalError("Subclass must implement `eat`")}
}
class Cow: Animal {
override func eat(_ food: Any) {
guard let food = food as? Hay else { fatalError("Cow cannot eat \(food)") }
}
}
class Horse: Animal {
override func eat(_ food: Any) {
guard let food = food as? Carrot else { fatalError("Horse cannot eat \(food)") }
}
}
class Chicken: Animal {
override func eat(_ food: Any) {
guard let food = food as? Grain else { fatalError("Chicken cannot eat \(food)") }
}
}
Any 로 모든 Food 타입을 받도록 강제할 수 있다.
그러나 런타임에 정확한 타입이 넘어왔는지 확인하도록 하위타입 implementation 에 너무 의존적이다
매개변수로 Food 가 아닌 타입을 넘길 수 있고, 이것은 또 다른 버그로 이어질 수 있다.
타입 파라미터를 사용해 각 서브타입에서 음식으로 지정할 타입에 대해 플레이스홀더를 제공해줄 수 있다.
이와 같은 방법으로 Animal 의 서브 클래스를 선언할 때 항상 Food 타입 파라미터에 들어갈 타입을 제공해주어야 한다.
그러나 음식을 먹는 것이 animal 의 core purpose 가 아니고, animal 과 관련된 많은 코드가 Food 타입과 관계없이 돌아갈 것이다.
또한 Animal 에 여러가지 기능이 추가된다고 생각해보자. 가령 Animal 에서 나오는 상품이나 (Commodity) 서식지(Habitat)등의 정보가 추가적으로 필요한 경우 아래와 같이 Animal 에 더 많은 타입 플레이스 홀더가 들어간다. 정말 끔찍하다. 그러면 Animal 의 서브타입을 정의할 때 마다 더 많은 타입 파라미터를 지정해주어야 한다.
class Animal<Food, Habitat, Commodity>
Build Interface
Animal 은 두 개의 공통적인 특성을 지니고 있다
각 동물에게는 특정 먹이 유형이 있고 그 음식 중 일부를 소비하는 작업도 있다
위 두 가지 공통적인 특성을 나타내기 위해 swift 에서 protocol 을 사용할 수 있다.
Protocol
protocol 은 conforming type 의 기능을 설명하는 추상화 도구이다.
protocol 을 사용하여 기능을 정의할 수 있고, 또한 기능과 실제 구현을 분리할 수 있다.
Subclass polymorphism 과 다르게, class 에 국한되어 있지 않고 enum, actor, struct 등의 다양한 타입에 사용될 수 있다.
채택하는 곳에서 associatedtype이 선언된 제네릭 프로토콜을 쓸 때면 typealias로 해당 타입을 구체화 해줘야한다(하지만 여기서는 안했음 타입앨리어스없이도 충분이추론가능한 경우엔 생략가능하기때문)
protocol 을 confrom 하면, 컴파일러가 해당 concrete Type 이 protocol 의 요구사항을 만족하는지 검사한다.
associatedType 이 사용된 eat 메소드의 구현부를 보고, 컴파일러가 conform type 에 사용된 associatedType 을 추론한다. (위의 경우엔 각 Animal conform type 이 eat 함수에서 사용한 Hay, Carrot, Grain 타입)
Animal protocol 로 추상화를 완료했기 때문에, 이제 농장에선 어떤 Animal 이던 하나의 함수로 먹이를 줄 수 있다.
Parametric polymorphism 을 이용해 feed 함수를 재정의해보자.
struct Farm {
func feed<A>(_ animal: A) where A: Animal {...}
// or
func feed<A: Animal>(_ animal:A) {...}
}
클래스, 구조체, enum 은 물론 함수에서도 타입 파라미터를 정의할 수 있다.
또한 타입 파라미터의 제약조건을 줄 수 있는데, 위에선 A 라는 타입을 받는다고 명시해주었고, 또한 A는 Animal protocol 을 conform 해야 한다는 제약조건을 지정해주었다.
where clause 절, 또는 과 같은 형식으로 제약 조건을 지정해 줄 수 있다.
이제 함수 feed 는 Animal protocol 을 conform 하는 모든 concrete Type 에 대해 사용할 수 있다.
타입 파라미터 더욱 간소화하기
func feed(_ animal: some Animal)
some Animal 문법으로 타입 파라미터나 where clause 절에 비해 코드가 간소화된 걸 확인할 수 있다.
swift 5.7 에서 some 키워드와 any 키워드에 대해 변화가 생겼다. 지금부터 some 키워드에 대해서 알아보자
What is some?
func feed(_ animal: some Animal)
some 키워드는 특정한 conform type 을 반환하거나 받을 때 사용할 수 있다.
SwiftUI 에서 View 구조체를 정의할 때 우리는 항상 some View 를 반환하는 body 프로퍼티를 정의한다.
some 키워드는 항상 conformance requirement 앞에 적힌다.
placeholder 타입을 나타내는 abstract type (some 과 같은) 은 opaque type (불명확 타입) 이라고 불린다.
그리고 이런 불명확 타입에 의해 대체되는 concrete type 은 underlying type 이라고 불린다.
하나의 opaque type 에 대응하는 underlying type 은 해당 opaque type 이 사용되는 범위 내에서 항상 동일하다.
func feed<A: Animal>(_ animal: A)
func feed(_ animal: some Animal)
위 두 가지 함수는 모두 opaque type 을 선언한다.
func getValue(Parameter) -> Result
opaque type 은 input 과 output 모두에 사용될 수 있다.
func getValue(Parameter) 같은 함수에서 타입 파라미터는 모두 input side 에 작성되는데, 따라서 함수를 호출하는 쪽에서 underlying type 을 결정한다.
보통 값을 넣어주는 쪽이 underlying type 을 결정하고, 값을 사용하는 쪽이 opaque type 과 같은 abstract type 을 바라본다.
Inferring the underlying type for some
let animal: some Animal = Horse()
위와 같은 지역변수에서, underlying type 은 대등호 우측에 있는 concrete type 에 의해 추론된다.
따라서 opaque type 을 가지는 지역 변수는 항상 초기값을 가지고 있어야 하며, 만약 초기값을 제공하지 않는다면 컴파일러는 에러를 보고한다.
아까도 말했듯이 opaque type 의 underlying type 은 해당 값의 사용 범위 내에서 항상 고정되어야 하며, 만약 바꾸려는 시도가 있으면 컴파일러는 에러를 보고한다.
var animal: some Animal = Horse()
animal = Cow() // Compiler Error 발생! underlying type 이 이미 Horse 로 결정된 상태에서 Cow 로 바꾸려고 시도하기 때문이다.
타입 파라미터는 언제 사용하는게 좋을까?
하나의 opaque type 을 여러 번 명시해야 할 때, 타입 파라미터를 사용하는게 좋다. 다음과 같은 예시를 보자
struct Silo<Material> {
private var storage: [Material]
init(storing materials: [Material]) {
self.storage = materials
}
}
var hayStorage: Silo<Hay>
위 코드에서 Material 이라는 opaque type 은 Silo 구조체 내부에서 여러 번 명시된 것을 확인할 수 있다. 이럴 땐 제네릭으로 타입 파라미터를 지정해 주는 것이 훨씬 편할 것이다.
이제 다시 generic code 를 작성하러 돌아가자
feed 함수 다시 작성하기
protocol Animal {
associatedtype Feed: AnimalFeed // <- grow static function 을 가지고 있는 추상화 프로토콜 이라고 생각하자.
func eat(_ food: Feed)
}
struct Farm {
func feed(_ animal: some Animal) {
let crop = type(of: animal).Feed.grow() // Animal 의 associatedtype에 접근하기 위해 type(of:) 를 사용할 수 있다.
let produce = crop.harvest() // 작물로부터 곡물을 획득한다.
animal.eat(produce) // opaque animal type 에 곡물을 먹인다.
}
}
opaque type 인 animal 의 underlying type 은 고정되어 있기 때문에, type(of:) 를 사용해 Animal 이 가지고 있는 associatedtype 에 접근할 수 있다.
associatedtype 인 Feed 타입도 underlying type 에 의해 추론되므로, 자유롭게 사용할 수 있다.
이것은 모두 underlying type(Animal 의 concrete type) 이 하나의 타입으로 고정되어 있기 때문에 가능하다. 컴파일러는 animal type, plant type, product type 간의 모든 관계를 알고 있다.
이러한 관계들은 우리가 동물에게 잘못된 먹이를 주는 것을 근본적으로 방지한다.
func feed(_ animal: some Animal) {
let crop = type(of: animal).Feed.grow()
let produce = crop.harvest()
animal.eat(Hay.grow().harvest()) // 만약 올바르지 않은 Feed 타입을 먹이려고 한다면, 컴파일러에 의해 에러가 발생한다.
}
마지막으로 모든 동물에게 먹이를 주는 feedAll 함수를 구현해보자!
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]) {
}
}
opaque type 의 underlying type 은 항상 특정한 하나의 타입으로 고정되야 한다고 계속 언급했었다.
위에서 animal 배열의 underlying type 도 항상 하나로 고정되어야 한다고 했다. 따라서 배열의 모든 원소는 똑같은 타입을 가지고 있어야 한다.
서로 다른 동물을 모두 담을 수 있는 배열을 정의해야한다
any 키워드
func feedAll(_ animals: [any Animal]) {
}
[some Animal] 을 [any Animal] 으로 바꿔보자!
any 키워드는 여러 가지 종류의 Animal 타입을 배열에 담을 수 있고, underlying type 또한 런타임에 실시간으로 변경될 수 있다는 것을 알려주는 키워드이다.
any 키워드 역시 conformance requirement 의 앞에 작성한다.
any 키워드를 통해 여러가지 concrete animal type 을 저장할 수 있는데, 이것을 통해 우리는 값 타입에서 subtype polymorphism 을 구현할 수 있다.
any 키워드를 사용해 여러 concrete type 을 하나의 표현식으로 작성하는 것을 type erasure라고 표현한다. type erasing 을 통해 컴파일 타임에선 concrete type 에 대해 알지 못하고, 런타임에 concrete type 에 대해 알게 된다.
다시 feedAll 메소드로 돌아가자!
우선 다양한 concrete type 을 가지는 any Animal 배열을 선언했으니, 해당 배열을 iterate 하자.
그 다음 iterate 하는 각 동물에게 먹이를 주기 위해 아래와 같이 Animal 프로토콜의 eat 메소드를 직접 호출해보자!
func feedAll(_ animals: [any Animal]) {
for animal in animals {
animal.eat(food: Animal.Feed) // 이 부분에서 컴파일 에러가 발생한다!
}
}
위 코드처럼 Animal 프로토콜의 eat 메소드를 직접 호출하면 컴파일 타임에 에러가 발생하는 것을 알 수 있다. 이유가 무엇일까?
우리는 animals 에 type erasure 를 사용하면서 컴파일러 타입이 underlying type 을 하나로 고정할 수 없다. 즉 Animal 이 가지고 있는 associatedtype 인 Feed 등의 타입 relationship 도 전부 해제된 상태인데, 따라서 컴파일 타임에 Animal Feed 타입이 어떤 concrete type 을 가지고 있는지 전혀 알 수가 없다.
따라서 우리는 각 animal 을 다시 underlying type 이 하나로 고정되는 context 로 옮겨야 한다. 이를 위해서 코드를 아래와 같이 수정해보자
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 으로 넘겨서 underlying type 을 unboxing 한다.
}
}
any Animal 과 some Animal 은 서로 다른 타입이다.
그러나 위 코드와 같이 any Animal 의 underlying type 을 some Animal 을 통해 unboxing 할 수 있다. 즉 underlying type 이 하나로 고정되지 않은 any Animal 타입을 some Animal 타입으로 넘겨줌으로써 underlying type 을 꺼낸 것이다.
1️⃣WWDC 영상 요약
Generic
Abstraction
Abstraction in swift
Swift 에서 Concrete Type 을 추상화할 수 있다.
여기서 Contcrete Type이란 구체화된 타입을 말하고 아래와 같은 경우 number의 타입이 Int로 구체화가 되었기때문에 이런경우 concrete type이라고 할 수 있다.
같은 아이디어를 가지지만 다른 구현 방법을 가진 여러 가지 타입이 존재할 때, 위와 같이 generic 을 활용해 모든 Concrete Type 에서 동작하는 abstract code 를 작성할 수 있다.
이제부터 아래 예제에서 swift 5.7 Generic 을 활용하는 방법을 알아보자!
WWDC영상에서 제공하는 코드 : 농장 시뮬레이션을 위한 코드
Model with concrete types
농장(Farm)이 있다고 생각해보자. 농장엔 소(Cow)가 있고, 소에게 먹일 곡물(Hay), 그리고 곡물을 수확할 식물(Alfalfa)이 있다.
feed - 식물(Alfalfa)에게서 곡물(Hay)을 수확하고, 수확한 곡물을 동물인 소(Cow)에게 먹임으로써 우리는 농장을 운영할 수 있다.
그러나 소말고도 아래와 같이 말, 닭과 같은 동물을 농장에서 추가적으로 기르려고 하면 어떻게 할까?
그러기 위해선 위와 같이 Horse, Chicken 을 나타내는 구조체를 추가로 선언하고, 각 동물에게 먹일 음식들도 추가적으로 선언해야 한다.
또한 농장(Farm) 에서 모든 동물에게 먹이를 먹일 수 있는데, 이것을 코드로 나타내면 엄청난 양의 Boiler Plate 코드가 탄생한다.
이때 여러분이 반복적인 구현으로 오버로드를 작성하고 있다면 일반화하라는 신호일 수 있습니다라고 이야기한다
Identify common capabilities
말과 닭, 소와 같은 동물 타입의 구조체들을 선언했을 때, 동물들은 모두 어떤 음식을 먹을 수 있는 eat 메소드를 가지고 있는 것을 확인할 수 있었다.
각 동물들은 서로 다른 음식을 먹고, 또한 음식을 먹는 방법 또한 다를 것이다.
우리는 여기서 eat 메소드를 abstract code 로 작성할 수 있고, eat 메소드가 정의된 concrete type 에 따라 다르게 동작하도록 정의할 수 있다.
abstract code 가 서로 다른 concrete type 에서 다르게 동작하는 것을 polymorphism (다형성) 이라고 한다.
Polymorphism - 다형성
다형성의 다양한 형태
Subtype polymorphism (서브타입 다형성) 을 이용해 위와 같은 문제를 해결해보자
위의 코드는 Animal이라는 클래스가 eat이라는 함수를 가지고있고 이를 상속받는 함수는 이 함수를 override할수있게된다 하지만 상속받는 클래스의 특징에 따라 food가 다르기 때문에 이부분을 해결해야한다는 문제를 가지고있다, 여기서는 구체적인 타입이 될수가 없구나 하는정도만 이해하고 우선은 이대로 넘어간다
그 다음 구조체로 선언된 각 동물들을 클래스 타입으로 바꾸고, Animal 슈퍼클래스를 상속시키자
Animal 슈퍼클래스를 상속한 동물 클래스들에서 eat 메소드를 재정의하자.
위와 같은 subtype polymorphism 의 단점
각 동물은 서로 다른 타입의 음식을 먹는다. 이것은 클래스 계층 구조로 표현하기 정말 어렵다
인스턴스 간 상태를 공유하고 싶지 않아도 강제로 Reference 타입인 class 타입을 사용해야 한다.
super class 의 method 를 재정의하지 않으면, 런타임 이전까지 에러를 알지 못한다. 왜냐면 class의 함수는 direct dispatch방식이 아니라 table dispatch방식이기때문에 런타임이 되서야 함수를 알 수 있게된다. 컴파일시점에서는 알수없기때문에 문제가 발생할 수 있다
각 동물이 서로 다른 타입의 음식을 먹는 것을 어떻게 표현할 수 있을까?
첫 번째 방법: Any 사용하기
두 번째 방법: Generic 사용하기
Build Interface
Protocol
Protocol 을 사용하여 Animal 을 표현해보자
associatedtype 은 type parameter 와 마찬가지로 concrete type 을 위한 플레이스홀더 역할을 한다.
associatedtype 은 protocol 을 conform 하는 타입에 의존적이다. (무슨 말인지 모르겠다면 아래에서 추가적인 예시를 보도록 하자)
eat 메소드는 associatedtype 으로 정의된 Feed 타입을 받는 메소드
protoocl 은 위와 같이 청사진만 제공해줄 뿐 실제 구현을 하진 않는다.
Protocol 을 conform 하는 Animal 타입들을 구현해보자.
Write a generic code
타입 파라미터 더욱 간소화하기
What is some?
opaque type 은 input 과 output 모두에 사용될 수 있다.
func getValue(Parameter) 같은 함수에서 타입 파라미터는 모두 input side 에 작성되는데, 따라서 함수를 호출하는 쪽에서 underlying type 을 결정한다.
보통 값을 넣어주는 쪽이 underlying type 을 결정하고, 값을 사용하는 쪽이 opaque type 과 같은 abstract type 을 바라본다.
Inferring the underlying type for some
타입 파라미터는 언제 사용하는게 좋을까?
이제 다시 generic code 를 작성하러 돌아가자
feed 함수 다시 작성하기
마지막으로 모든 동물에게 먹이를 주는 feedAll 함수를 구현해보자!
any 키워드
[some Animal] 을 [any Animal] 으로 바꿔보자!
any 키워드는 여러 가지 종류의 Animal 타입을 배열에 담을 수 있고, underlying type 또한 런타임에 실시간으로 변경될 수 있다는 것을 알려주는 키워드이다.
any 키워드 역시 conformance requirement 의 앞에 작성한다.
any 키워드를 통해 여러가지 concrete animal type 을 저장할 수 있는데, 이것을 통해 우리는 값 타입에서 subtype polymorphism 을 구현할 수 있다.
다시 feedAll 메소드로 돌아가자!