Swift5.9で導入されるAttached Macroについて

## 概要

[SE-0389 Attached Macros](https://github.com/apple/swift-evolution/blob/main/proposals/0389-attached-macros.md) 内で取り上げられていた5種類のMacro、`Peer Macro`, `Member Macro`, `Accessor Macro`, `Member Attribute Macro`, `Conformance Macro`について調査しまとめてみました。

これらのMacroについては、[swift-macro-examples](https://github.com/DougGregor/swift-macro-examples) にて非常に分かりやすくまとまっていたのでぜひ手元で試してみることをオススメします。またMacroを理解する上、また実際に実装する上で [swift-syntax](https://github.com/apple/swift-syntax) についてざっくり理解する必要があるので、[ドキュメント](https://swiftpackageindex.com/apple/swift-syntax/508.0.1/documentation/swiftsyntax)を読みつつ理解すると良いです。

### Peer Macro

Peer Macroは、プロパティやメソッドに対して付与し、その付与した対象の情報を入力に受け取り新たなプロパティやメソッドを生成するマクロです。

以下のようにprotocol定義がなされています。

```swift
// ref: https://github.com/apple/swift-syntax/blob/main/Sources/SwiftSyntaxMacros/MacroProtocols/PeerMacro.swift
public protocol PeerMacro: AttachedMacro {

  static func expansion<
    Context: MacroExpansionContext,
    Declaration: DeclSyntaxProtocol
  >(
    of node: AttributeSyntax,
    providingPeersOf declaration: Declaration,
    in context: Context
  ) throws -> [DeclSyntax]
}
```

ここで `declaration` に付与した対象に関する情報が入り、返り値の `DeclSyntax` （任意の宣言文）を展開します。

例えば、async関数に対して付与することで、completionHandlerを引数に追加した同期関数を展開するPeer Macro、 `AddCompletionHandlerMacro` を定義することが可能です。（実際の実装は少々長いため省略します。）

```swift
// ref: https://github.com/apple/swift-evolution/blob/main/proposals/0389-attached-macros.md#peer-macros
// implementation example: https://github.com/DougGregor/swift-macro-examples/blob/main/MacroExamplesPlugin/AddCompletionHandlerMacro.swift
public struct AddCompletionHandlerMacro: PeerDeclarationMacro {
  public static func expansion(
    of node: CustomAttributeSyntax,
    providingPeersOf declaration: some DeclSyntaxProtocol,
    in context: some MacroExpansionContext
  ) throws -> [DeclSyntax] {
    // make sure we have an async function to start with
    // form a new function "completionHandlerFunc" by starting with that async function and
    //   - remove async
    //   - remove result type
    //   - add a completion-handler parameter
    //   - add a body that forwards arguments
    // return the new peer function
    return [completionHandlerFunc]
  }
}
```

そしてこれをasync関数の `fetch` に対して付与することで、以下の展開が行われます。

```swift
@AddCompletionHandler
func fetch(limit: Int) async -> Item {
	return b
}

// コンパイル時に下記の関数が展開される
func fetch(limit: Int, completionHandler: (Item) -> Void) {
	Task {
		completionHandler(await fetch(limit: limit))
	}
}
```

他にも swift-macro-examples には、上記と真逆の展開を行う `AddAsync` マクロという実装例があるので、ぜひ見てみると良いかと思います。

[swift-macro-examples/AddAsyncMacro.swift at main · DougGregor/swift-macro-examples](https://github.com/DougGregor/swift-macro-examples/blob/main/MacroExamplesPlugin/AddAsyncMacro.swift)

### Member Macro

Member Macroは `actor`, `class`, `struct` , `enum`, `extension` , `protocol` に対して付与することで、付与対象の情報 (`DeclGroupSyntax`) を用いて新たにメンバーを追加することが可能なマクロです。

以下のようにprotocol定義がなされています。

```swift
// ref: https://github.com/apple/swift-syntax/blob/main/Sources/SwiftSyntaxMacros/MacroProtocols/MemberMacro.swift
public protocol MemberMacro: AttachedMacro {

  static func expansion<
    Declaration: DeclGroupSyntax,
    Context: MacroExpansionContext
  >(
    of node: AttributeSyntax,
    providingMembersOf declaration: Declaration,
    in context: Context
  ) throws -> [DeclSyntax]
}
```

入力として `AttributeSyntax` （ここではマクロ自身） , `DeclGroupSyntax`  （マクロの付与対象）を受け取り、`DeclSyntax` （任意の定義、例えば関数や変数、typealias等々。詳しくは [こちら](https://swiftpackageindex.com/apple/swift-syntax/508.0.1/documentation/swiftsyntax/declsyntax#declarations)）の配列を返し新たにメンバーを追加することが可能です。

例として [swift-macro-examples](https://github.com/DougGregor/swift-macro-examples/blob/main/MacroExamplesPlugin/CaseDetectionMacro.swift) から、Enumの各caseを判定するcomputed propertyを追加する `CaseDetectionMacro` は以下のように実装されています。

```swift
// ref: https://github.com/DougGregor/swift-macro-examples/blob/main/MacroExamplesPlugin/CaseDetectionMacro.swift
public struct CaseDetectionMacro: MemberMacro {
  public static func expansion<
    Declaration: DeclGroupSyntax, Context: MacroExpansionContext
  >(
    of node: AttributeSyntax,
    providingMembersOf declaration: Declaration,
    in context: Context
  ) throws -> [DeclSyntax] {
    declaration.memberBlock.members
      .compactMap { $0.decl.as(EnumCaseDeclSyntax.self) }
      .map { $0.elements.first!.identifier }
      .map { ($0, $0.initialUppercased) }
      .map { original, uppercased in
        """
        var is\(raw: uppercased): Bool {
          if case .\(raw: original) = self {
            return true
          }

          return false
        }
        """
      }
  }
}
```

Enum内の各case情報にアクセスし、それを元に新たなメンバーの定義を追加することが可能です。

```swift
// ref: https://github.com/DougGregor/swift-macro-examples/blob/main/MacroExamples/main.swift
@CaseDetection
enum Pet {
  case dog
  case cat(curious: Bool)
  case parrot
  case snake
}

let pet: Pet = .cat(curious: true)
print("Pet is dog: \(pet.isDog)")
print("Pet is cat: \(pet.isCat)")
```

### Accessor Macro

Accessor Macroは、Stored Propertyに対して付与することで、getterやsetter などのアクセッサーをマクロ展開が可能なマクロです。

以下のようにprotocol定義がなされています。

```swift
protocol AccessorMacro: AttachedMacro {
  /// Expand a macro described by the given attribute to
  /// produce accessors for the given declaration to which
  /// the attribute is attached.
  static func expansion(
    of node: AttributeSyntax,
    providingAccessorsOf declaration: some DeclSyntaxProtocol,
    in context: some MacroExpansionContext
  ) async throws -> [AccessorDeclSyntax]
}
```

### **Member attribute Macro**

Member Attribute Macroは、Member Macro同様に `class` や `struct` 等に付加することで、その内部に持つプロパティや関数等に attribute を展開するマクロです。

以下のようにprotocol定義がなされています。

```swift
public protocol MemberAttributeMacro: AttachedMacro {

  static func expansion<
    Declaration: DeclGroupSyntax,
    MemberDeclaration: DeclSyntaxProtocol,
    Context: MacroExpansionContext
  >(
    of node: AttributeSyntax,
    attachedTo declaration: Declaration,
    providingAttributesFor member: MemberDeclaration,
    in context: Context
  ) throws -> [AttributeSyntax]
}
```

付与対象が `declaration` に、付与対象に含まれるプロパティやメソッドが `member` （単体）で入力され、 その`member` に対してAttributeをマクロ展開することが可能です。

`@objcMembers` が各プロパティやメソッドに `@objc` を付ける挙動と似ています。

実際に既存の`@objcMembers`をMemberAttributeMacroを用いて実装すると以下のようになります。

```swift
public struct MyObjcMembersMacro: MemberAttributeMacro {

  public static func expansion(
    of node: AttributeSyntax,
    attachedTo declaration: some DeclGroupSyntax,
    providingAttributesFor member: some DeclSyntaxProtocol,
    in context: some MacroExpansionContext
  ) throws -> [AttributeSyntax] {

    let varDecl = member.as(VariableDeclSyntax.self)
    let funcDecl = member.as(FunctionDeclSyntax.self)

    guard varDecl != nil || funcDecl != nil else {
      return []
    }

    return [
      AttributeSyntax(
        attributeName: SimpleTypeIdentifierSyntax(
          name: .identifier("objc")
        )
      )
    ]
  }
}

@attached(memberAttribute)
public macro MyObjcMembers() = #externalMacro(module: "NameOfModule", type: "MyObjcMembersMacro")

@MyObjcMembers
class ObjcExposedClass: NSObject {
	let ok: Bool = true
}

// no compile error
Hoge().responds(to: #selector(getter: ObjcExposedClass.ok))
```

また return には他のAttribute MacroやPeer Macroを指定することも可能です。

### **Conformance** Macro

Conformance Macroは、Macroを付与する対象に特定のConformanceを付与することが可能なマクロです。

以下のようにprotocol定義がなされています。

```swift
// ref: https://github.com/apple/swift-syntax/blob/main/Sources/SwiftSyntaxMacros/MacroProtocols/ConformanceMacro.swift
public protocol ConformanceMacro: AttachedMacro {

  static func expansion<
    Declaration: DeclGroupSyntax,
    Context: MacroExpansionContext
  >(
    of node: AttributeSyntax,
    providingConformancesOf declaration: Declaration,
    in context: Context
  ) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)]
}
```

以下の例はEquatableに適合するようにするマクロです。

これだけでは何も意味を持ちませんが、条件に応じて適合を変更することが可能である点や、ある適合を前提としたMember Macro等に対してextensionで ConformanceMacro を適合する等のユースケースがありそうです。

```swift
struct EquatableMacro: ConformanceMacro {
  public static func expansion(
    of attribute: AttributeSyntax,
    providingConformancesOf decl: some DeclGroupSyntax,
    in context: some MacroExpansionContext
  ) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] {

    return [("Equatable", nil)]
  }
}
```