Why macros?
Swift는 사용자가 표현력이 좋은 코드를 쓰기를 원한다.
그래서 프로토콜 요구사항 유도(Equatable, Codable등), result builder 등의 기능을 지원한다.
이들은 모두 Swift컴파일러가 코드를 자동적으로 변환해주는 방식으로 동작한다.
근데 기존 기능이 요구사항에 맞지 않는다면?
그래서 매크로가 추가되었다.
C(혹은 Objective-C) 매크로를 아는 사람들에게는 복잡한 심경일 수 있다.
Design philosophy
Translation model
Swift는 매크로 호출을 보게되면 코드에서 이를 추출해서 매크로 구현체가 있는 컴파일러 플러그인으로 이를 보낸다.
func printAdd(_ a: Int, b: Int) {
let (result, str) = #stringfy(a + b)
print("\\(str) = \\(result)")
}
printAdd(1,2)
플러그인은 별도의 샌드박스 환경에서 돌아가고 Swift로 작성되어 있다.
매크로 코드를 받아서 펼쳐진 코드(expansion)을 반환한다.
func printAdd(_ a: Int, b: Int) {
let (result, str) = (a + b, "a + b")
print("\\(str) = \\(result)")
}
printAdd(1,2)
컴파일러는 expansion을 받아서 기존 코드와 함께 컴파일한다.
Swift는 매크로의 존재를 어떻게 알까?
매크로 선언이 있어서 가능하다.
함수처럼 이름과 시그니처, 반환값을 가진다.
추가적으로 매크로의 역할을 정의하는 attribute를 1개 이상 필수적으로 가진다.
/// Creates a tuple containing both the result of `expr` and its source code represented as a
/// `String`.
@freestanding(expression)
macro stringify<T>(_ expr: T) -> (T, String)
Macro roles
매크로에 적용되는 규칙 모음
궁극적으로는 매크로 확장을 예측 가능하게 하는 책임을 가지는 요소다.
role종류
@freestanding(expression): 값을 반환하는 코드를 만든다.
expression: 실행해서 결과를 만들어내는 코드 단위
let numPixels = (x + width) * (y + height)
// ^~~~~~~~~~~~~~~~~~~~~~~~~~ This is an expression
// ^~~~~~~~~ But so is this
// ^~~~~ And this
ex. force unwrap할 때 이유를 반드시 남기게 하기
// 이유가 안남는다.
let image = downloadedImage!
// 이유를 남겼지만, 좀 번거롭다.
guard let image = downloadedImage else {
preconditionFailure("Unexpectedly found nil: downloadedImage was already checked")
}
매크로를 사용해서 이 코드를 간략화할 수 있다.
/// Force-unwraps the optional value passed to `expr`.
/// - Parameter message: Failure message, followed by `expr` in single quotes
@freestanding(expression)
macro unwrap<Wrapped>(_ expr: Wrapped?, message: String) -> Wrapped
// 호출부
let image = #unwrap(downloadedImage, message: "was already checked")
// 코드가 펼쳐진 형태
{ [downloadedImage] in
guard let downloadedImage else {
preconditionFailure(
"Unexpectedly found nil: ‘downloadedImage’ " + "was already checked",
file: "main/ImageLoader.swift",
line: 42
)
}
return downloadedImage
}()
@freestanding(declaration): 하나 이상의 선언을 만든다.
2차원 배열을 표현하는 타입을 별도로 만든다고 생각해보자.
public struct Array2D<Element>: Collection {
public struct Index: Hashable, Comparable { var storageIndex: Int }
var storage: [Element]
var width1: Int
public func makeIndex(_ i0: Int, _ i1: Int) -> Index {
Index(storageIndex: i0 * width1 + i1)
}
public subscript (_ i0: Int, _ i1: Int) -> Element {
get { self[makeIndex(i0, i1)] }
set { self[makeIndex(i0, i1)] = newValue }
}
public subscript (_ i: Index) -> Element {
get { storage[i.storageIndex] }
set { storage[i.storageIndex] = newValue }
}
// Note: Omitted additional members needed for 'Collection' conformance
}
근데 쓰다보니 3차원 배열도 필요해졌다.
public struct Array3D<Element>: Collection {
public struct Index: Hashable, Comparable { var storageIndex: Int }
var storage: [Element]
var width1, width2: Int
public func makeIndex(_ i0: Int, _ i1: Int, _ i2: Int) -> Index {
Index(storageIndex: (i0 * width1 + i1) * width2 + i2)
}
public subscript (_ i0: Int, _ i1: Int, _ i2: Int) -> Element {
get { self[makeIndex(i0, i1, i2)] }
set { self[makeIndex(i0, i1, i2)] = newValue }
}
public subscript (_ i: Index) -> Element {
get { storage[i.storageIndex] }
set { storage[i.storageIndex] = newValue }
}
// Note: Omitted additional members needed for 'Collection' conformance
}
나중에 더 높은 차원의 배열이 필요해지면 점점 복잡해질거다.
public struct Array2D<Element>: Collection { ... }
public struct Array3D<Element>: Collection { ... }
public struct Array4D<Element>: Collection { ... }
public struct Array5D<Element>: Collection { ... }
각 타입은 선언이기 때문에 declaration macro를 사용해서 자동화해보자.
/// Declares an `n`-dimensional array type named `Array<n>D`.
/// - Parameter n: The number of dimensions in the array.
/// expression이 아니기 떄문에 반환값은 없다.
@freestanding(declaration, names: arbitrary)
macro makeArrayND(n: Int)
/// 사용
#makeArrayND(n: 2)
#makeArrayND(n: 3)
#makeArrayND(n: 4)
#makeArrayND(n: 5)
/// expansion
public struct Array2D<Element>: Collection {
public struct Index: Hashable, Comparable { var storageIndex: Int }
var storage: [Element]
var width1: Int
public func makeIndex(_ i0: Int, _ i1: Int) -> Index {
Index(storageIndex: i0 * width1 + i1)
}
public subscript (_ i0: Int, _ i1: Int) -> Element {
get { self[makeIndex(i0, i1)] }
set { self[makeIndex(i0, i1)] = newValue }
}
public subscript (_ i: Index) -> Element {
get { storage[i.storageIndex] }
set { storage[i.storageIndex] = newValue }
}
}
@attached(peer): 현재 선언을 보고 상응하는 또 다른 선언을 만들어준다.
구현 자체는 금방한다.
/// Fetch the avatar for the user with `username`.
func fetchAvatar(_ username: String) async -> Image? {
...
}
func fetchAvatar(_ username: String, onCompletion: @escaping (Image?) -> Void) {
Task.detached { onCompletion(await fetchAvatar(username)) }
}
매크로를 쓰면 금방 끝난다.
/// Overload an `async` function to add a variant that takes a completion handler closure as
/// a parameter.
@attached(peer, names: overloaded)
macro AddCompletionHandler(parameterName: String = "completionHandler")
/// Fetch the avatar for the user with `username`.
@AddCompletionHandler(parameterName: "onCompletion")
func fetchAvatar(_ username: String) async -> Image? {
...
}
/// expansion
/// Fetch the avatar for the user with `username`.
/// Equivalent to ``fetchAvatar(username:)`` with
/// a completion handler.
func fetchAvatar(
_ username: String,
onCompletion: @escaping (Image?) -> Void
) {
Task.detached {
onCompletion(await fetchAvatar(username))
}
}
@attached(accessor): 프로퍼티에 접근자를 만들어준다.
변수나 첨자 선언에 붙을 수 있다.
get, set, willSet, didSet 등의 접근자를 만들어준다.
ex. dictionary를 backing store로 가지고, 특정 값만 접근하게 만드는 경우
struct Person: DictionaryRepresentable {
init(dictionary: [String: Any]) { self.dictionary = dictionary }
var dictionary: [String: Any]
var name: String {
get { dictionary["name"]! as! String }
set { dictionary["name"] = newValue }
}
var height: Measurement<UnitLength> {
get { dictionary["height"]! as! Measurement<UnitLength> }
set { dictionary["height"] = newValue }
}
var birthDate: Date? {
get { dictionary["birth_date"] as! Date? }
set { dictionary["birth_date"] = newValue as Any? }
}
}
접근자를 일일이 쓰는 게 번거롭다.
여기서 프로퍼티 래퍼는 쓸 수 없다. 프로퍼티 래퍼는 다른 프로퍼티들에 접근할 수 없기 때문이다.
그래서 매크로로 만든다.
/// Adds accessors to get and set the value of the specified property in a dictionary
/// property called `storage`.
@attached(accessor)
macro DictionaryStorage(key: String? = nil)
struct Person: DictionaryRepresentable {
init(dictionary: [String: Any]) { self.dictionary = dictionary }
var dictionary: [String: Any]
@DictionaryStorage var name: String
@DictionaryStorage var height: Measurement<UnitLength>
@DictionaryStorage(key: "birth_date") var birthDate: Date?
}
@attached(memberAttribute): 적용된 타입 혹은 확장의 멤버에 attribute를 추가해준다.
ex. 위에서 @DictionaryStorage Attribute가 반복되는 것도 번거롭다.
그래서 memberAttribute 매크로를 기존 매크로에 붙여준다.
/// Adds accessors to get and set the value of the specified property in a dictionary
/// property called `storage`.
@attached(memberAttribute)
@attached(accessor)
macro DictionaryStorage(key: String? = nil)
@DictionaryStorage
struct Person: DictionaryRepresentable {
init(dictionary: [String: Any]) { self.dictionary = dictionary }
var dictionary: [String: Any]
var name: String
var height: Measurement<UnitLength>
@DictionaryStorage(key: "birth_date") var birthDate: Date?
}
struct Person: DictionaryRepresentable {
init(dictionary: [String: Any]) { self.dictionary = dictionary }
var dictionary: [String: Any]
@DictionaryStorage
var name: String
@DictionaryStorage
var height: Measurement<UnitLength>
@DictionaryStorage(key: "birth_date") var birthDate: Date?
}
@attached(member): 적용된 타입 혹은 확장에 새로운 멤버를 선언해준다.
stored property와 enum case모두 가능
ex. 생성자와 프로퍼티 선언 자동화
/// Adds accessors to get and set the value of the specified property in a dictionary
/// property called `storage`.
@attached(member, names: named(dictionary), named(init(dictionary:)))
@attached(memberAttribute)
@attached(accessor)
macro DictionaryStorage(key: String? = nil)
// The @DictionaryStorage member macro
@DictionaryStorage struct Person: DictionaryRepresentable {
var name: String
var height: Measurement<UnitLength>
@DictionaryStorage(key: "birth_date") var birthDate: Date?
}
/// expansion
@DictionaryStorage struct Person: DictionaryRepresentable {
init(dictionary: [String: Any]) {
self.dictionary = dictionary
}
var dictionary: [String: Any]
var name: String
var height: Measurement<UnitLength>
@DictionaryStorage(key: "birth_date") var birthDate: Date?
}
@attached(comformance): 적용된 타입 혹은 확장에 프로토콜을 채택해준다.
ex. DictionaryRepresentable 자동채택
/// Adds accessors to get and set the value of the specified property in a dictionary
/// property called `storage`.
@attached(conformance)
@attached(member, names: named(dictionary), named(init(dictionary:)))
@attached(memberAttribute)
@attached(accessor)
macro DictionaryStorage(key: String? = nil)
@DictionaryStoragestruct Person
{
var name: String
var height: Measurement<UnitLength>
@DictionaryStorage(key: "birth_date") var birthDate: Date?
}
// expansion
struct Person: DictionaryRepresentable
{
var name: String
var height: Measurement<UnitLength>
@DictionaryStorage(key: "birth_date") var birthDate: Date?
}
role 합성
Macro implementation
매크로 선언 뒤에 = 을 붙이고 그 뒤에 구현체를 명시한다.
/// Creates a tuple containing both the result of `expr` and its source code represented as a
/// `String`.
@freestanding(expression)
macro stringify<T>(_ expr: T) -> (T, String) = #externalMacro(
module: "MyLibMacros",
type: "StringifyMacro"
)