티스토리 뷰

Swift

Swift 5.6 ) Introduces existential any

Zedd0202 2022. 4. 3. 15:14
반응형

 

안녕하세요 :) Zedd입니다.

코로롱🦠에 걸려서 이래저래 굉장히 힘드네요 🥲 진짜 안걸리는게 제일 좋다~~!!!! 조심~~!!!

오늘은 Swift 5.6에서 추가된 Introduces existential any 요 기능만 호다닥 보려고 합니다.


먼저, Existential type를 알아야 합니다. 

 

# Existential type

직역하여 "실존 타입"정도로..이해하면 될 것 같아요. 

우와 새로운 개념 ㄷㄷ;;;;이 아니라 Existential type은 그냥 용어일 뿐, 우리가 계속 써왔던 개념입니다.

protocol Pet {

  var name: String { get }
}

struct Cat: Pet {

  var name: String
}

Pet이라는 프로토콜이 있고, Pet을 준수하는 Cat이 있습니다. 

Cat은 여기서 concrete type이라고 할 수 있겠죠. 

var someCat = Cat(name: "Yumi")

이렇게 Cat타입으로 someCat을 정의했습니다.

하지만 Cat이 Pet을 준수하고 있기 때문에,

var somePet: Pet = Cat(name: "Yumi")

이 코드도 전혀 문제없습니다.

concrete type인 Cat 대신에 Pet타입으로 somePet을 정의한것이죠. 

여기서!!!! Pet을 existential type이라고 합니다. 

Cat이라는 실존(존재)하고 있는 구조체를 참조하는 프로토콜인거죠. 

 

Swift 공식문서 - Protocol 중 Protocols as Types에 가보면, 

Using a protocol as a type is sometimes called an existential type
프로토콜을 타입으로 사용하는 것은 existential type이라고 불리기도 한다. 

요렇게 나와있네요 ㅎㅎ

var somePet: Pet = Cat(name: "Yumi")

아무튼 이 경우 Pet은 existential type입니다.

existential type을 파라미터로 사용하는 경우도 있죠.

func 사료를_주다(to pet: Pet) {}

이런식으로 말이죠!

우리가 계속 써왔던거죠!? 

 

# Generic과 Existential type

Pet에 함수를 추가해주겠습니다.

protocol Pet {
   func eat()
}

🤔 : 내가 Pet을 준수하는 타입을 만들어서 넘기면, eat을 호출해주는 함수를 만들어야겠어

 

1️⃣ : Generic을 쓰자!

func 사료를_주다<T: Pet>(to pet: T) {
    pet.eat()
}

Generic 함수에 protocol constraint를 걸어놨습니다.

이제 이 함수는 Pet을 준수하는 concrete type만 받을 수 있게 됩니다. 

그럼 Pet을 준수하는 concrete type을 만들어서 사료를_주다를 호출해봅시다. 

struct Cat: Pet {
    
    let name: String
    
    func eat() { print("웅냥ㄴ양얀양냥") }
}

func 사료를_주다<T: Pet>(to pet: T) {
    pet.eat()
}

let cat = Cat(name: "Yumi")
사료를_주다(to: cat) // 웅냥ㄴ양얀양냥

위 코드는 문제 없이 잘 돌아갑니다.

하지만 아래와같은 코드.. existential type으로 사용한 코드는 컴파일 에러를 일으킵니다. 

let cat: Pet = Cat(name: "Yumi")
사료를_주다(to: cat) // 🚨 Protocol 'Pet' as a type cannot conform to the protocol itself

나는 Pet을 준수하는 타입만 받도록 했는데, Pet 프로토콜 그 자체가 넘겨졌기 때문입니다.

 

func 사료를_주다<T: Pet>(to pet: T) {
    pet.eat()
}
let cat = Cat(name: "Yumi")
사료를_주다(to: cat) // 웅냥ㄴ양얀양냥

위 코드가 컴파일될 때, Swift는 이렇게 생각합니다.

1. Cat 인스턴스를 만듬

2. Cat 타입을 사료를_주다로 넘김

3. Cat의 eat()을 호출함

 

🥸(Swift) : 엥 그냥 Cat에 있는 eat()을 호출하면 되겠군..응 cat.eat()이야~~

Swift는 그냥 Cat의 eat()을 직접(direct) 호출하는 최적화된 코드를 생성할 수 있게 됩니다.

컴파일시간에 Swift는 Cat의 eat()을 호출해야한다는 것을 아는것이죠.

이것이 바로 static dispatch입니다.

컴파일타임에 어떤 구현을 실행하도록 결정할 수 있다면, 이를 static dispatch라고 합니다.
컴파일러가 실제로 어떤 구현이 실행될건지 알기때문이죠. 즉, inline과 같은것을 포함하여 코드를 적극적으로 최적화 할 수 있게 됩니다.
이러한 static dispatch는 dynamic dispatch와는 대조적인데요,
dynamic dispatch는 컴파일러가 컴파일타임에 어떤 구현을 실행할건지 결정 할 수 없습니다.
런타임때만!!!! 실제로 구현된 곳으로 jump하게 되죠(jump to it. jump라는 말이 너무 찰떡이라 굳이 번역하지 않았..)
따라서 dynamic dispatch의 비용은 static dispatch보다 훨씬 큽니다.

Swift ) (2) Understanding Swift Performance (Swift성능 이해하기) 참고

 

 

2️⃣ : existential type을 쓰자!

🤣 : ㅋㅋ 언제 <T: Pet> 이렇게 쎀ㅋㅋㅋ;;; 그냥

func 사료를_주다(to pet: Pet) {
    pet.eat()
}

이렇게 쓰면 되는뎅ㅎㅎㅎㅋㅋ

func 사료를_주다(to pet: Pet) {
    pet.eat()
}
let cat = Cat(name: "Yumi")
사료를_주다(to: cat) // 웅냥ㄴ양얀양냥

ㅎㅎ근데 1번에 비해 

let cat: Pet = Cat(name: "Yumi")
사료를_주다(to: cat) // 웅냥ㄴ양얀양냥

이렇게 Pet을 existential type으로 사용한 것도 들어가긴 하는데~~  상관없지 모;

 

🥸 : 삐빅- dynamic dispatch입니다.

 

자 아래 코드를 다시 봅시다.

func 사료를_주다(to pet: Pet) {
    pet.eat()
}
let cat: Pet = Cat(name: "Yumi")
사료를_주다(to: cat) // 웅냥ㄴ양얀양냥

위 코드가 컴파일 될 때 Swift는 어떤 타입의 eat()이 호출될지 모릅니다. 

왜냐면 cat인스턴스는 Cat타입이 아니라 Pet타입으로 지정되어 있기 때문이죠(existential type)

Generic을 사용했을 때 처럼 코드 최적화를 하지 못하고, 런타임에 어떤 eat()을 호출해야할지 알수 있게 됩니다. 

즉, dynamic dispatch를 해야합니다. 

어떤 타입의 eat()을 호출해야하는지 알기 위해 Swift는 existential container라는 것도 만들고, Heap도 사용하게 됩니다. (무조건 사용하는건 아니지만) 

즉 대충 dynamic dispatch는 static dispatch에 비해 비용이 많이 든다는거..

dynamic dispatch 과정을 더 자세히 알고싶다면, Swift ) (2) Understanding Swift Performance (Swift성능 이해하기) 를 참고해주세요. 


🤔 : 내가 Pet을 준수하는 타입을 만들어서 넘기면, eat을 호출해주는 함수를 만들어야겠다. 

// 1️⃣ : Generic을 쓰자!
func 사료를_주다<T: Pet>(to pet: T) {
    pet.eat()
}

// 2️⃣ : existential type을 쓰자!
func 사료를_주다(to pet: Pet) {
    pet.eat()
}

2번이 1번보다 훨씬 비용이 많이 듬에도 불구하고

1. minimal spelling. Existential type이 Generic에 비해 더 적게 씀.

2. 코드상으로 existential type에 대한 비용이 전혀 드러나지 않음. 

위와같은 이유때문에 사람들은 주로 2번을 사용하게 됩니다. 

그로인해 개발자들은 이 둘을 혼동하게 됩니다. (엥 같은거 아냐?; 라는 식이라던가) 

 

Introduce existential any proposal에서는 이를 

language makes existential types too easy to reach for (...)
언어(Swift겠죠)가 existential types에 너무 쉽게 도달하도록 만든다..


라고 표현합니다. 

또한, existential type을 사용하는 비용은 숨겨져서는 안되며, 개발자들은 이러한 의미 체계를 명시적으로 선택해야 한다는 뜻에서...

existential any를 소개합니다.

 

# existential any

이제 Swift 5.6에서는 existential type과 any를 같이 쓸 수 있게 됩니다. (나중에는 무조건 이렇게 써야함)

func 사료를_주다(to pet: ✅any✅ Pet) {
    pet.eat()
}

구구절절 앞 설명이 길었지만, existential type을 사용할 때 이제 any를 같이 써야한다는 그런 변경사항입니다.

😀 : 아 진짜요

🤓 : 단순한 변화같지만..Swift에 아주 큰 변화가 생긴겁니다. Swift 5.6에서는 별 다른 경고나 에러는 없지만..... 향후 Swift 버전에서는 경고가 생성될 예정이며 Swift 6부터는 에러를 낼 계획 ^^! 

🤭 : ⁉️

// Swift 5 mode

protocol P {}
protocol Q {}
struct S: P, Q {}

let p1: P = S() // 'P' in this context is an existential type
let p2: any P = S() // 'any P' is an explicit existential type

let pq1: P & Q = S() // 'P & Q' in this context is an existential type
let pq2: any P & Q = S() // 'any P & Q' is an explicit existential type
// Swift 6 mode

protocol P {}
protocol Q {}
struct S: P, Q {}

let p1: P = S() // 🚨 error 🚨
let p2: any P = S() // ✅ okay ✅

let pq1: P & Q = S() // 🚨 error 🚨
let pq2: any P & Q = S() // ✅ okay ✅

 

😀 : 나 이렇게 쓴 코드 개많은데.. 진심이야? 

func 사료를_주다(to pet: Pet) {
    pet.eat()
}

🥸 : ㅇㅇSwift 6부터는 에러날거임 

 

# 참고

✔️ 프로토콜이 아닌 타입에 대해 any를 붙히면 에러가 납니다.

struct S {}

let s: any S = S() // 🚨 error: 'any' has no effect on concrete type 'S'

func generic<T>(t: T) {
  let x: any T = t // 🚨 error: 'any' has no effect on type parameter 'T'
}

let f: any ((Int) -> Void) = generic // 🚨 error: 'any' has no effect on concrete type '(Int) -> Void'

[정리]

1. 프로토콜

2. 프로토콜 composition

3. metatypes 

앞에만 any를 붙힐 수 있음.

 

✔️ any는 Any 및 AnyObject에는 안붙혀줘도 됩니다. (protocol composition의 일부가 아닌 이상)

근데 붙혀도 에러는 나지 않음.. 

struct S {}
class C {}

let value: any Any = S()
let values: [any Any] = []
let object: any AnyObject = C()

protocol P {}
extension C: P {}

let pObject: any AnyObject & P = C() // okay

 

🤔 : 에엥 

protocol P {}
extension C: P {}

let pObject: any AnyObject & P = C() // okay

P앞에 왜 any없어.. any넣어야할 거 아니야

이때는 protocol composition이라 앞에만 넣어주면 됨. 

let pObject: any AnyObject & any P = C() // 🚨 error: 'any' should appear at the beginning of a composition

넣으면 에러납니다. 

 

✔️ Metatypes

protocol P {}
struct S: P {}

let existentialMetatype: any P.Type = S.self

protocol Q {}
extension S: Q {}

let compositionMetatype: any (P & Q).Type = S.self

let protocolMetatype: (any P).Type = (any P).self

P.Type -> any P.Type

P.self -> (any P).self

 

자세한 내용은 [SE-0335] Introduce existential any를 참고해주세요 :D

틀린 내용이 있다면 댓글 부탁드립니다~ 

그건 그렇고..유미가 Yumi가 아니라 Yuumi 였군요?! 귀찮으니 그냥 둔다

 

[참고]

- Introduce existential any proposal

- https://www.hackingwithswift.com/swift/5.6/existential-any

 

반응형

'Swift' 카테고리의 다른 글

[Swift 5.7] Multi-statement closure type inference  (1) 2022.07.09
[Swift 5.7] if let shorthand  (1) 2022.07.09
Swift ) Sequence  (1) 2022.02.05
Literal  (0) 2022.01.23
Swift ) prefix / suffix  (1) 2021.12.01