티스토리 뷰

Swift

dynamicCallable

Zedd0202 2021. 3. 18. 12:15
반응형

 

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

오늘은...!! dynamicCallable 공부!

 

dynamicCallable


🧑‍💻 Q : dynamicMemberLookup 이랑 비슷한 느낌...

🙋 A : 맞습니다..비슷한 친구입니다.

 

[특징]

✔️ SE-0216 에서 처음 소개. Swift 5.0에서 구현됨. (참고 Swift 5.0 변경사항)

(DynamicMemberLookup proposal - SE-0195 의 후속조치임)

✔️ 타입을 callable하게 만듬

 

# dynamicCallable

1. 만약 dynamicCallable을 "사용"하는 타입이 있다면,

2. 이 타입의 인스턴스를 호출할 수 있게 된다.

 

1. 만약 dynamicCallable을 "사용"하는 타입이 있다면,

바로 예제로 보겠습니다. dynamicMemberLookup에서 본 예제입니다.

struct Contact {
    
    var persons: [String: String]
 }

dynamicMemberLookup이 @dynamicMemberLookup attribute를 타입앞에 붙혔던 것 처럼, 

dynamicCallable역시 @dynamicCallable attribute를 타입 앞에 붙히면 됩니다.

@dynamicCallable ✅
struct Contact {

    var persons: [String: String]
}

이렇게요. 

이러면 이제 이 타입은 "callable type"이 되게 됩니다.

 

이제 이런 오류가 나실텐데요,

dynamicMemberLookup이 subscript메소드를 요구했던것 처럼,

dynamicCallable도 요구하는 메소드가 있습니다. 

func dynamicallyCall(withArguments args: <#Arguments#>) -> <#R1#>

func dynamicallyCall(withKeywordArguments args: <#KeywordArguments#>) -> <#R2#>

이렇게 2가지 중 1개를 구현해야만합니다.

return은 있어도 없어도 상관없습니다.

위 메소드들을 구현할 때 제약이 몇가지 있습니다만...지금 바로 보진 않을게요. 일단 예제를 보고 익숙해집시다!

 

그럼 저는 상단에 있는 친구를 구현해주겠습니다. 

@dynamicCallable
struct Contact {

    var persons: [String: String]
    
    func dynamicallyCall(withArguments args: [String]) {

    }
}

이렇게요. 

필요한 메소드를 구현했으니 에러는 없어집니다.

 

2. 이 타입의 인스턴스를 호출할 수 있게 된다.

사실 이걸 어떻게 설명해야하나...고민을 했는데, 이해가 안가시더라도 예제로 보면 될 것 같습니다.

let contact = Contact(persons: [
    "지구": "Zedd",
    "달": "Marshmello"
])
contact() ✅ 가능 

initializer가 아닙니다. contact라는 인스턴스를 "호출"할 수 있게 된거죠.

이 뿐만이 아닙니다.

contact()
contact("지구")
contact("지구", "달")

이런것도 가능해집니다.

왜냐면 파라미터 타입으로 [String]을 넣고있기 때문이죠.

func dynamicallyCall(withArguments args: [String]) {
 
}

[String]이 아니고 그냥 아무 타입이든 넣어도 됩니다.

func dynamicallyCall(withArguments args: [Int]) {
 
}

let contact = Contact(persons: [
    "지구": "Zedd",
    "달": "Marshmello"
])

contact()
contact(1)
contact(2, 3)

Int면 이런식으로 넣어야겠죠? 

 

🧑‍💻 Q : 지금 dynamicallyCall이 [Int]을 요구하고 있는데, 왜 

contact()
contact(1)
contact(2, 3)

이런식으로 넣어주는거야?

contact([])
contact([1])
contact([2, 3])

이게 맞지 않아? 

🙋 A : 사실상

contact()
contact(1)
contact(2, 3)

이 코드는

contact.dynamicallyCall(withArguments: [])
contact.dynamicallyCall(withArguments: [1])
contact.dynamicallyCall(withArguments: [2, 3])

이 코드의 syntactic sugar이므로..사실상 배열화(?)가 된 상태라고 보면 될 것 같습니다.

 

위에서 보셨다시피 

저렇게 인스턴스를 호출하게 되면 

func dynamicallyCall(withArguments args: [AnyObject]) -> AnyObject

func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, AnyObject>) -> AnyObject

이 dynamicallyCall메소드가 불리게 되는데요, 우리가 아까 구현해준 dynamicallyCall에 로직을 추가해보겠습니다. 

func dynamicallyCall(withArguments args: [String]) {
    args.forEach {
        print(self.persons[$0])
    }
}

받은 args를 돌면서 person에 해당 arg key를 가진 value를 print합니다. 

결과는 당연히

contact()
contact("지구") // prints Optional("Zedd")
contact("지구", "달") // prints Optional("Zedd") Optional("Marshmello")

이렇게 됩니다.

 

대충 어떤 느낌으로 사용하는지 아시겠나요? 

 

# 메소드 제약사항

[1] func dynamicallyCall(withArguments args: <#Arguments#>) -> <#R1#>

[2] func dynamicallyCall(withKeywordArguments args: <#KeywordArguments#>) -> <#R2#>

그럼 위 메소드들의 제약사항을 보겠습니다.

 

[1]

func dynamicallyCall(withArguments args: <#Arguments#>) -> <#R1#>

withArguments에는 ExpressibleByArrayLiteral를 준수하는 타입만 들어갈 수 있습니다.

우리가 위에서 봤듯이 가장 만만한 Array가 들어갈 수 있겠죠.

ex.

func dynamicallyCall(withArguments args: [String]) -> 임의의 타입
func dynamicallyCall(withArguments args: [Int]) -> 임의의 타입

리턴타입에는 특별한 제약은 없습니다.

 

[2]

func dynamicallyCall(withKeywordArguments args: <#KeywordArguments#>) -> <#R2#>

withArguments ExpressibleByDictionaryLiteral를 준수하는 타입만 들어갈 수 있습니다.

가장 만만한 Dictionary가 들어갈 수 있습니다. 1번과 마찬가지로 리턴타입에는 특별한 제약이 없습니다. 

ex. 

func dynamicallyCall(withKeywordArguments args: [String: String]) -> 임의의 타입

또는 KeyValuePair가 들어갈 수 있습니다. (KeyValuePair가 ExpressibleByDictionaryLiteral를 준수하고 있으므로)

func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, String>) -> 임의의 타입

 

근데 [2] 메소드 같은 경우에는 Key의 타입이 무조건 String이어야 하더라구요..!? 

 

[2] 메소드도 사용해봅시다.

func dynamicallyCall(withKeywordArguments args: [String: String]) {
    args.forEach { print(persons[$0.value]) }
}
contact()
contact("지구") // prints Optional("Zedd")

비슷하게 사용하면 됩니다!

 

🙋 Q : persons[$0.key]가 아니라 print(persons[$0.value]인 이유?

🧑‍💻 A: 사실상 

contact()
contact("지구")

이 코드는

contact.dynamicallyCall(withKeywordArguments: [:])
contact.dynamicallyCall(withKeywordArguments: ["": "지구"])

이 코드의 syntactic sugar입니다. 제가 준 값이 key가 아닌 value로 들어가게 됩니다. 

 

Q : key가 왜 ""로 들어가?

A : 그냥 스펙입니다..!? .따로 key를 지정해주지 않으면 빈 문자열이 key로 들어가게 됩니다.

 

그래서 만약

@dynamicCallable
struct Contact {

    var persons: [String: String]

    func dynamicallyCall(withKeywordArguments args: [String: String]) {
        args.forEach { print(persons[$0.value]) }
    }
}
...
contact("지구", "달")

이런 코드를 넣었다면..런타임 에러가 발생하게 됩니다.

왜냐면 

contact.dynamicallyCall(withKeywordArguments: ["": "지구", "": 달])

이렇게 desugar 되기 때문이죠. 그래서 duplicate keys에러가 발생하게 됩니다.

이 에러를 내고싶지 않다면

 

[1]

contact()
contact("지구") // prints Optional("Zedd")
contact(a: "지구", b: "달") // prints Optional("Zedd") Optional("Marshmello")

이렇게 key를 지정해주거나 

[2] 

@dynamicCallable
struct Contact {

    var persons: [String: String]

    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, String>) {
        args.forEach { print(persons[$0.value]) }
    }
}

let contact = Contact(persons: [
    "지구": "Zedd",
    "달": "Marshmello"
])
contact()
contact("지구") // prints Optional("Zedd")
contact("지구", "달") // prints Optional("Zedd") Optional("Marshmello")

dynamicallyCall 메소드에 Dictionary대신 중복 key를 허용하는 KeyValuePairs타입을 넣으면 됩니다. 

 

# 메소드 2개 다 구현해보기

[1] func dynamicallyCall(withArguments args: <#Arguments#>) -> <#R1#>

[2] func dynamicallyCall(withKeywordArguments args: <#KeywordArguments#>) -> <#R2#>

 

 

둘 중 하나의 메소드만 구현해도 컴파일에러가 사라졌었는데요,

둘 다 구현해보겠습니다.

@dynamicCallable
struct Contact {

    var persons: [String: String]
    
    func dynamicallyCall(withArguments args: [String]) {
        args.forEach {
            print("\(self.persons[$0]) Array")
        }
    }
    
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, String>) {
        args.forEach {
            print("\(self.persons[$0.value]) KeyPair")
        }
    }
}

let contact = Contact(persons: [
    "지구": "Zedd",
    "달": "Marshmello"
])
contact()
contact("지구") 
// Optional("Zedd") Array

contact("지구", "달") 
// Optional("Zedd") Array
// Optional("Marshmello") Array

contact(a: "지구", b: "달") 
// Optional("Zedd") KeyPair
// Optional("Marshmello") KeyPair

그냥 이렇다~~만 봐주세요 ㅎㅎ

 

참고 

github.com/apple/swift-evolution/blob/master/proposals/0216-dynamic-callable.md

반응형

'Swift' 카테고리의 다른 글

Swift 5.4 Released!  (3) 2021.04.28
Introducing Swift Collections  (0) 2021.04.13
dynamicMemberLookup  (0) 2021.03.11
Property Wrapper  (2) 2021.03.07
Key-Value Observing(KVO) in Swift  (2) 2021.03.01