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
@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): 프로퍼티에 접근자를 만들어준다.
@attached(memberAttribute): 적용된 타입 혹은 확장의 멤버에 attribute를 추가해준다.
@attached(member): 적용된 타입 혹은 확장에 새로운 멤버를 선언해준다.
@attached(comformance): 적용된 타입 혹은 확장에 프로토콜을 채택해준다.
Macro implementation
writing corrent macros