Swift Macro

Hyunndy·2023년 7월 6일
0

WWDC 2023

https://developer.apple.com/videos/play/wwdc2023/10164/?time=574

Swift 5.9에서 새롭게 나온 개념

  • 언어 자체의 기능을 확장 (expressive APIs)
  • 보일러플레이트 코드 제거
    Swift expression 더 활용

예시 1️⃣ Assert

assert(max(a,b) == c)

는 조건이 거짓이면 프로그램을 중지시키지만, 정확히 무엇이 잘못되었는지에 대한 정보를 거의 얻지 못한다.
디버거에 로깅 추가하거나, 프로그램을 trap 해야 한다.

이걸 개선하면?

XCAssertEqual(max(a, b), c)

적어도 두 값이 같지 않는다는건 알 수 있다.

하지만 둘 중 어떤값이 문제인지는 알 수 없다.
max(a, b)인지 c인지
뭐가 잘못된것인지?

다시 assert로 돌아가서...
우린 assert가 실패했을 때 알고싶은 정보가 너무 많다.
a,b,c의 값은 무엇이고 Max는 어떤걸 리턴했는지?
이런 정보를 얻기위해 사용자 정의 기능없이는 개선을 못했는데,
매크로를 사용하면 가능해진다.

wwdc예시에서
#assert 구문은 assert라는 매크로를 확장한다.
이 매크로는 assert가 fail이 될 때 더 많은 경험을 제공할 수 있다.


이렇게.

API

매크로는 API이므로 매크로를 정의하는 모듈을 가져와 액세스할 수 있다.
패키지 형태로 배포된다.

assert 매크로의 경우 PowerAssert라는 패키지를 까보면 내부 코드를 볼 수 있는데,
이런식으로 되어있다.

Public macro assert(_ condition: Bool)

import PowerAssert
#assert(max(a, b) == c)

macro 키워드와 함께 써있지만, 그외에는 함수처럼 보인다.

매크로가 값을 생성하면, Result Type은 일반적인 화살표 구문으로 작성된다.

external Macro

대부분의 매크로는 문자열을 통해 매크로 구현을 위한 모듈 및 type을 지정하는 "external macro"(외부 매크로)로 정의된다.

public macro assert(_ condition: Bool) = #externalMacro(
	module: "PowerAssertPlugin",
    type: "PowerAssertMacro"
)

external macro의 타입은 Compiler 플러그인 역할을 하는 "별도의 프로그램"에서 정의된다.

  1. Swift Compiler는 매크로 사용을 위한 소스 코드를 플러그인에 전달합니다.
  2. 플러그인은 새로운 소스코드를 생성한 다음 Swift 프로그램에 다시 통합된다.

위 사진에서 매크로는 Assertion을 코드에 확장시켜 개별 값을 캡쳐하고, 소스코드에서 보여야할 곳에 위치시킨다.
매크로가 대신 보일러플레이트 코드를 작성해준다.

Role

매크로 선언에는 하나의 추가정보인 "Macro Role"이 있다.

@freestanding(expression)
public macro assert(_ condition: Bool) = #externalMacro(
	module: "PowerAssertPlugin",
    type: "PowerAssertMacro"
)

여기서 assert 매크로는 freestanding(독립된) expression(식) 매크로 이다.

  1. 왜 freestanding(독립형)이라고 할까?
    (#) 해시 그문을 사용하고 해당 구문에서 directly하게 작동하여 새 코드를 생성하기 때문에 독립형이라고 한다.
  1. 왜 expression(식)이라고 할까?
    값을 생성할 수 있는 모든 곳에서 사용할 수 있는 매크로 식이기 때문에

예시 2️⃣ Fondation Predicate API

Foundation Predicate API는 expression 매크로의 훌륭한 예시 이다.

// Predicate expression macro

let pred = #Predicate<Person> {
	$0.favoriteColor == .blue
}

let blueLovers = people.filter(pred)

predicate 매크로를 사용하면 클로저를 사용하여 type-safe한 방식으로 조건자(Predicate)를 작성할 수 있다.

predicate의 결과 값은 아주 많은 API와 함께 사용될 수 있다.
예를들어, Swift Collection 작업인 SwiftUI와 SwiftData를 포함해서

// Predicate expression macro

@freestanding(expression)
public macro Predicate<each Input>(
_ body: (repeat each Input) -> Bool
) -> Predicate<repeat each Input> 

let pred = #Predicate<Person> {
	$0.favoriteColor == .blue
}

let blueLovers = people.filter(pred)

macro 자체는 input type set(입력 유형 집합)에 대해 generic 하다.

(repeat each Input) -> Bool

는 밑에 예시의
{ 
	$0.favoriteColor == .blue
}

해당 입력 유형의 값에 대해 작동하는 함수인 클로저 인수를 통해 Boolean 값을 반환한다.
input의 집합이 일치한가 아닌가에 대한 Boolean 값. (예시에서는 favoriteColor가 blu인지에 대한..)

-> Predicate<repeat each Input>

let pred = #Predicate<Person> { $0.favoriteColor == .blue }

그리고 App의 다른곳에서 사용할 수 있는 Predicate형 인스턴스를 반환한다.

그럼 이 인스턴스를 받아서..

let blueColorPred = #Predicate<Person> { $0.favoriteColor == .blue }
let bluePerson = personList.filter(blueColorPred)

요런식으로 filter Operation과 사용할 수 있음이다.
오호라!


애플이 매크로를 내놓은 이유는 보일러플레이트 코드를 매크로를 대체해서 코드를 줄이기 위함

예시 3️⃣ Enum Case

// Testing for a specific enum case
enum Path {
	case relative(String)
    case absolute(String)
}

애플은 코드 내에서 열거형을 많이 사용한다는 것을 알게 되었다!

이런 Path enum이 있다고 할 때,
collection에서 absolute path만 필터링 해야하는 등의 특정 사례를 확인해야 하는 경우가 종종 있다.

어떻게 해야할까?
Path의 extension으로 특정 Case일 때만 filter를 걸 수 있게 Boolean 변수를 추가할 수 있다.

// Testing for a specific enum case
enum Path {
	case relative(String)
    case absolute(String)
}

let absPaths = paths.filter { $0.isAbsolute }

extension Path {
	var isAbsolute: Bool {
    	if case .absoulte = self { true }
        else { false } 
    }
}

extension Path {
	var isAbsolute: Bool {
    	if case .absoulte = self { true }
        else { false } 
    }
}

오우..하지만 case가 추가될 때 마다 보일러플레이트 코드가 늘어나게 된다.


extension Path {
	var isAbsolute: Bool {
    	if case .absoulte = self { true }
        else { false } 
    }
}

extension Path {
	var isRelative: Bool {
    	if case .relative = self { true }
        else { false } 
    }
}

이런식으로.

여기서 매크로를 사용해 보일러플레이트를 없앨 수 있다.

@CaseDetection
enum Path {
	case relative(String)
    case absolute(String)
}

let absPaths = paths.filter { $0.isAbsolute } 

@CaseDetection은 프로퍼티 래퍼처럼 custom-attribute 구문을 사용하여 작성된. attached 매크로이다.

Attached Macro

Attached Macro란 적용된 syntax 그 자체(여기서는 enum 자체)를 Input으로 사용하여 새 코드를 작성한다.

이 Macro-expanded 코드는 normal한 스위프트 코드이다.
컴파일러가 프로그램에 통합하는.
요 코드들을 마음대로 쓸 수 있다.

Attached Macro Roles

Attached Macro는 첨부된 declaratin(선언)을 확장하는 방법에 따라 5가지 역할로 분류할 수 있다.

  • @attached(member)
    - 위의 @CaseDetection도 "멤버" attached 매크로 중 하나.
    • 즉, type(유형, enum같은..)또는 extension(확장)에서 새 member를 생성한다는 의미
  • @attached(peer)
    - 연결된 declaration의 반대를 생성하기 위한 새 Declaration을 추가한다.
    • 예를들어, 비동기 함수의 completion handler 버전을 생성
  • @attached(accessor)
    - stored property를 computed 속성으로 전환할 수 있으며, property 액세스에 대한 특정 작업을 수행하거나 property wrapper와 비슷하지만 더 유연하게 추상화 시킬 수 있다.
  • @attached(memberAttribute)
    - Type의 특정 멤버에 attribute를 제공
  • @attached(conformance)
    - 새로운 Protocol을 conform 시킬 수 있다. 여러 attached macro roles들을 함께 구성해서 극대화 시킬 수 있음.
    그 중 한 예시가 observation.
    Observation은 SwiftUI 중 하나 이다.

    예시 4️⃣ Observation

    final class Person: ObservableObject {
    	@Published var name: String
       @Published var age: Int
       @Published var isFavorite: Bool
    }
    struct ContentView: View {
    	@ObservedObject var person: Person
       
       var body: some View {
       	Text("Hello, \(person.name)")
       }
    }

Observation은 SwiftUI 중 하나 이다.
클래스의 속성에 대한 변경 사항을 관찰하려면
class가 ObservableObject를 준수하도록 만들고 모든 Property를 Published로 마크하고, 뷰에서 ObservedObject wrappr을 사용하기만 하면 된다.

길다 길어!!
여기서 매크로를 사용한다면?

@Observable final class Person {
	var name: String
    var age: Int
    var isFavorite: Bool
}

struct ContentView: View {
	var persoon: Person
    
    var body: some View {
    	Text("Hello, \(person.name)")
    }
}

@Observable 매크로를 클래스에 연결하면 모든 StoredProperty를 관찰할 수 있습니다.

Observable 매크로는 3가지 macro roles로 작동한다.

@attached(member, names: ...)
@attached(memberAttribute)
@attached(conformance)
public macro Observable() = #externalMacro(...)
@Observable final class Person {
	var name: String
    var age: Int
    var isFavorite: Bool
}

각 Macro Role은 Person 클래스가 Observable 매크로에 의해 보강되는 특정 방식에 해당합니다.

@Observable 매크로의 코드는 이런식으로 되어있고,
사실은

@Observable final class Person: Observable {
	@ObservationTracked var name: String { get {...} set {...} }
	@ObservationTracked var age: Int { get {...} set {...} }
	@ObservationTracked var isFavorite: Bool { get {...} set {...} }
}

이런식으로 되어있는데, 이게 Observable 매크로 안에 다 접혀있는것이다..!


프로그램에 미치는 영향을 더 잘 이해하기 위해 매크로가 어떻게 확장되는지 확인해야 될 때 마다 XCode에서 바로 확인 가능

profile
https://hyunndyblog.tistory.com/163 티스토리에서 이사 중

0개의 댓글