iOS

some 과 any 그리고 Any?(2)

스엠 2022. 10. 26. 21:50

책 읽는게 재밌어서 책만 읽고 개발쪽은 약간 도외시 했었네요.

경제, 금융 이야기가 왜케 재밌는지...

각설하고 some에 이어서 any에 대해서도 알아보겠습니다.

 

any

any는  swift 5.6에서 처음으로 등장한 신택스 입니다. 

some을 알기 전에  Opaque Type을 알아야 했듯이 any를 알기 전에는 Existential Type을 먼저 알아야 합니다. 

 

Existential Type

직역 하면 실존? 타입 정도가 되겠죠?

Existential Type이라는 용어도 이번에 any라는 용어를 도입하면서 처음으로 생긴 단어입니다. 

그래서 새로운 거냐구요? 그건 또 아닙니다. 우리가 익히 써왔던 문법에 명칭을 새로 만들어줬다고 생각하면 됩니다. 

그럼 Existential Type이 뭘까요? 

Protocol A { 
} 

Protocl B { 

} 

Struct C : A, B { 

} 


let c1 : A = C()
let c2 : B = C()

//코드에서 프로토콜인 A, B가 바로 Existential Type 입니다

swift  document에 의하면 

Using a protocol as a type is sometimes called an existential type, which comes from the phrase “there exists a type T such that T conforms to the protocol.

: 프토로콜을 타입으로 사용할 경우  Existential Type이라고 부르는데 이는 다음 구절 "T가 프로토콜을 준수하도록 하는 타입 T가 존재(실존)한다"

 

이게 Existential Type의 끝입니다. 쉽죠? 

 

그러면 이미 있는 문법인데 왜 특별히 any가 필요하나 싶기도 합니다. 

이에 대한 해답은 swift 5.6에서 any를 소개하면서 나온 부연 설명을 읽어보면 해답이 나옵니다. 

 

Despite these significant and often undesirable implications, existential types have a minimal spelling. Syntactically, the cost of using one is hidden, and the similar spelling to generic constraints has caused many programmers to confuse existential types with generics. In reality, the need for the dynamism they provided is relatively rare compared to the need for generics, but the language makes existential types too easy to reach for, especially by mistake. The cost of using existential types should not be hidden, and programmers should explicitly opt into these semantics.

:구문적으로 제너릭을 쓰는 방법과 Extential Type을 쓰는 방법이 똑같아 혼동의 여지를 주었다. -> 따라서 코드의 가독성을 높이기 위하여 any를 도입하였다

 

예시를 들어보면 다음과 같습니다. 

var a : A
var b : B

요렇게 2개가 있을때  A,B가 concrete Type인지 Extential Type인지 구분 할 수 있나요? 

또는

func attack(with : Weapon : Weapon){ } 

func attack<WeaponP : p>(p : WeaponP){ }

위의 2상황에서 Weapon이 Existential Type인지 WeaponP가 뭔지 알 수 있을까요? 

이처럼 제너릭을 쓰는 경우와 Existential Type을 쓰는 방법이 똑같아서  any를 도입했다고 볼 수 있습니다. 

 

이제 가독성의 차이가 있는 것은 알겠는데 some과 같은거 아니여? 그냥 generic 써도 되는거 아니여? 이렇게 생각할 수 도 있습니다. 

우선 generic 과  any의 차이점 부터 보겠습니다. (이부분은 최애 블로그 중 하나인 제드님의 블로그 참고했습니다)

protocol Pet { 
	func eat()
}


//generic 

func feed<T : pet>(to pet : T){ 
   pet.eat()
}


struct Cat : Pet { 
	 let name : String 
     func eat() { print("\(name) is eating") } 
}

//1 번 
let cat = Cat(name : "cat")
feed(to : cat)
// result : cat is eating

//2 번
let cat : Pet =  Cat(name : "cat")
feed(to : cat) // Protocol 'Pet' as a type cannot conform to the protocol itself
// error

 

1번과 2번의 차이는 뭘까요? 

feed에 Pet라는 프로토콜을 채택하고 있는 클라스 혹은 구조체를 받기로 정의되어 있는데  2번의 경우 프로토콜 그 자체가 들어왔기 때문에 에러가 발생하는 것입니다.

 

이때 1번은 컴파일 과정에서 어떻게 추론될까요?

1. Cat 인스턴스 생성 

2. Cat타입을 feed함수로 넘김

3. Cat의 eat()을 호출함

 

컴파일러 입장에서는 Cat이라는 concrete Type이 들어왔으니 그냥 Cat의 eat()을 호출하면 되겟구나라고 생각합니다. 

컴파일 단계부터 Cat의 eat()을 호출해야한다는 것을 알고 있는 static dispatch가 적용되어 있는 것이죠.

static -> 정적 뭔가 보기부터 최적화 되어있는것 같습니다. 좋은거죠

 

그러면 any의 경우를 정상적인 코드로 실행되게 해봅시다.

func feed(to pet : T){ 
   pet.eat()
}


struct Cat : Pet { 
	 let name : String 
     func eat() { print("\(name) is eating") } 
}
¥
let cat : Pet =  Cat(name : "cat")
feed(to : cat)
// result : cat is eating

요게 끝입니다. feed함수의 제너릭을 없애버리고 그냥 Existential Type을 받게 바꾸어 버린거죠.

그럼 이때 컴파일러는 어떻게 행동할까요?

 

일단 제너릭과의 차이점은 Pet이라는 프로토콜이 넘어왔냐 아니면 Concrete Type이 넘어왔냐의 차이입니다. 

여기서 cat은 Cat의 type이 아니라 Pet의 타입인 상태죠, 즉 Existential Type이라는 것입니다. 

따라서 컴파일러는 코드 최적화를 하지 못합니다. 런타임중에 부랴부랴 찾아서 어? 이제보니 Cat이구나 하고 Cat의 eat()을 호출해야하는 거지요. 

이것이 dynamic dispatch입니다. 

static <-> dynamic 딱봐도 최적화 되어 있지 않아 보이죠.

 

하지만 제너릭에 비해 훨씬더 쓰기 간편하지 않나요? 이러한 이유로 우리는 개발내내 Existential type을 써오면서 퍼포먼스적인 비용은 생각치 못하고 있었던 것입니다. 그리고 코드 상에서 둘이 차이점이 명확히 들어나지 않으니 두개를 혼동하고 같은거로 오해하며 써왔던 거죠.

따라서 any를 키워드를 이용해 이거는 비용이 많이 들어 꼭 써야 되는거 아니면 쓰지마 이런식으로 유도하기 위해서 나온것입니다. 

 

그럼 some과 any의 차이점은 무엇일까요?

 

간단하게 표로 살펴보면 될것 같습니다. 

some any
고정된 concrete Type을 갖고 있는다.
holds a fixed concrete type
 임의의 concrete Type을 들고 있는다.
holds an arbitraty concrete type
타입에 대한 관계를 보장해줍니다.
guarantees type relation Ship
타입에 대한 관계를 지워줍니다.
erases type relationships

 

극적인 예시는 배열에서 사용할때 차이가 납니다. 

전 글에서 some은 이런 특징을 가지고 있다고 했습니다. 

  • protocol인데 특정한 identity가 필요한 경우 예를들어 배열 같은 경우

코드를 통해 예시를 들어보겠습니다. 

//alfalfa -> 자주개자리 식물 
//Hay -> 건초
struct Hay { 
	static func grow() -> Alfalfa{ ..} 
}

struct Alfalfa { 
	func harvest() -> Hay {...}
}



protocol Animal { 
	associatedtype Feed : AnimalFeed
    func eat(_ food: Feed)
}

struct Cow : Animal { 
	func eat(food: Hay){
    	.....
    }
}

struct Seed { 
	static func grow() -> Carrot
}

struct Carrot { 
	func harvest() -> ChoppedCarrot
}

struct ChoppedCarrot { 
	let seed 
    let chopped 
}

struct Rabbit : Animal { 
	func eat(food : ChoppedCarrot){ 
    	....

    }
}

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] { 
    
    }
    
    func feedAll2(animals : [any Animal] { 
    
    }
}

여기에서  feedAll의 경우  [some animal]로 되어 있는 것을 볼 수 있습니다. 

이렇게 되면 토끼와 소가 있는데 [소, 소, 소, 소] 혹은 [토끼, 토끼, 토끼, 토끼] 밖에 안됩니다. 

이유는 some은 underlying type이 fixed 즉 기저가 되는 타입이, concrete type이, 고정되어 있기 때문입니다. 

 

하지만  feedAll2 의 [any Animal] 경우  any로 인해서 type relationship이 erased 되었기 때문에  [토끼, 소, 토끼, 소] 될 수 있음을 알 수 있습니다. 

 

그럼 이제 feedAll2의 함수를 다음과 같이 구성할 수 있습니다. 

func feedAll2(animals : [any Animal]) { 
	for animal in animals { 
    	feed(animal)
    }
}

이렇게 될 수 있는 이유는 컴파일 내부에서 any로 들어온 것은 some으로 바꿀 수 있기 때문입니다. 

 

이렇게 some과 any에 대해서 알아보았습니다. 

뭔가 자유로운 분위기 속에서 규칙을 점점 더 확고히 잡아갈려는 Swift 개발진들의 의도를 엿볼 수 있었던 시간이었습니다. 

 

 

 

'iOS' 카테고리의 다른 글

KeyChain vs UserDefaults(2)  (0) 2022.11.22
KeyChain vs UserDefaults(1)  (0) 2022.11.17
some 과 any 그리고 Any?(1)  (0) 2022.10.12
ARC란 무엇인가?(3)  (0) 2022.05.21
ARC란 무엇인가?(2)  (0) 2022.05.21