• Why macros?

    • Swift는 사용자가 표현력이 좋은 코드를 쓰기를 원한다.

      • 그래서 프로토콜 요구사항 유도(Equatable, Codable등), result builder 등의 기능을 지원한다.

      • 이들은 모두 Swift컴파일러가 코드를 자동적으로 변환해주는 방식으로 동작한다.

        스크린샷 2023-06-16 오전 11.14.17.png

    • 근데 기존 기능이 요구사항에 맞지 않는다면?

      • 컴파일러에 기능을 추가할 수도 있지만, 이를 위해서는 프로젝트 리더와의 미팅 및 여타 다른 프로젝트 리더들과 논의하는 복잡한 과정을 거쳐야 한다.
    • 그래서 매크로가 추가되었다.

      • 컴파일러를 바꾸지 않고도 Swift에 자체적인 기능을 추가할 수 있다.
      • 번거롭고 지겨운 보일러플레이트를 쉽게 제거할 수 있다.
      • swift package를 통해서 컴파일러 변경 없이 이를 다른 곳에 배포할 수 있다.
    • C(혹은 Objective-C) 매크로를 아는 사람들에게는 복잡한 심경일 수 있다.

      • 강력하긴 하지만, 그 한계와 함정 또한 있기 때문에
      • 하지만 Swift 매크로는 그런 이슈들을 회피하는 방향으로 C 매크로와는 굉장히 다르게 돌아간다.
  • Design philosophy

    • 매크로를 쓰고 있는 곳은 매크로를 쓰고 있다는 게 티가 잘 나야 한다.
      • Freestanding 매크로: expression이나 declaration을 대체하는 매크로
        • #으로 시작한다.
      • Attached 매크로: 다른 declaration에 붙이는 용도(attribute)
        • @으로 시작한다.
      • 원래 두 가지 모두 컴파일러의 특수한 동작을 위해서 사용되었던 키워드고, 매크로는 이를 확장성 있게 만든 것이다.
      • #과 @이 없으면 매크로가 사용되지 않은 것이다.
    • 매크로로 넘어가는 코드와 매크로로 만들어지는 코드 모두 완전한 Swift코드여야 하고, 타입 체크 및 검증이 가능해야한다.
      • 불완전한 expression을 쓸 수도 없고, 함수처럼 인자와 반환값의 타입 모두 검사된다.
      • 매크로는 입력을 검증해서 컴파일 경고나 오류를 내보낼 수 있다.
    • 매크로는 기존 프로그램을 변경하지 않고 추가만 하는 방식으로 동작해야 한다.
    • 매크로의 동작은 마법같이 이뤄지면 안된다
      • Xcode에서 매크로가 쓰인 곳에 우클릭하면 즉석에서 펼쳐줄 수 있다.
      • 펼쳐놓은 것에다가 브레이크포인트를 걸고 디버깅도 가능하다.
        • 이는 매크로 소스코드가 공개되어 있지 않아도 가능하다.
        • 실제로 Preview 매크로도 소스코드는 공개되어 있지 않지만, 펼치기로 어떻게 코드가 만들어지는지는 볼 수 있다.
      • 매크로에 유닛테스트도 작성가능하고, 이를 권장한다.
  • Translation model

    • Swift는 매크로 호출을 보게되면 코드에서 이를 추출해서 매크로 구현체가 있는 컴파일러 플러그인으로 이를 보낸다.

      func printAdd(_ a: Int, b: Int) {
      	let (result, str) = #stringfy(a + b)
      	print("\\(str) = \\(result)")
      }
      
      printAdd(1,2)
      
    • 플러그인은 별도의 샌드박스 환경에서 돌아가고 Swift로 작성되어 있다.

      • 매크로 코드를 받아서 펼쳐진 코드(expansion)을 반환한다.

        스크린샷 2023-06-16 오후 2.49.36.png

        func printAdd(_ a: Int, b: Int) {
        	let (result, str) = (a + b, "a + b")
        	print("\\(str) = \\(result)")
        }
        
        printAdd(1,2)
        
    • 컴파일러는 expansion을 받아서 기존 코드와 함께 컴파일한다.

      • 그래서 프로그램을 돌릴 때는 매크로를 호출하지 않고 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): 하나 이상의 선언을 만든다.

        • 함수, 변수, 타입 등
        • ex. N차원 배열 타입 만들기
          • 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
            }
            
          • 나중에 더 높은 차원의 배열이 필요해지면 점점 복잡해질거다.

            • 하지만 제네릭, 프로토콜 확장, 서브클래스 및 어떤 Swift 기능도 이 문제를 다루기에는 약간 부족하다.
            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): 현재 선언을 보고 상응하는 또 다른 선언을 만들어준다.

        • 함수, 변수, 타입 뿐 아니라 import나 operator 선언에도 붙일 수 있다.
        • 멤버에다가 붙이면 멤버로 만들어지고, top-level 선언에 붙이면 top-level 선언이 만들어진다.
        • ex. async 함수에 대응되는 completionHandler 버전 만들기
          • 구현 자체는 금방한다.

            /// 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로 가지고, 특정 값만 접근하게 만드는 경우
        • 접근자를 일일이 쓰는 게 번거롭다.
        • 여기서 프로퍼티 래퍼는 쓸 수 없다. 프로퍼티 래퍼는 다른 프로퍼티들에 접근할 수 없기 때문이다.
        • 그래서 매크로로 만든다.
      • @attached(memberAttribute): 적용된 타입 혹은 확장의 멤버에 attribute를 추가해준다.

      • @attached(member): 적용된 타입 혹은 확장에 새로운 멤버를 선언해준다.

      • @attached(comformance): 적용된 타입 혹은 확장에 프로토콜을 채택해준다.

    • role 합성
  • Macro implementation

  • writing corrent macros