LoginSignup
24
15

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

Posted at

概要

SE-0389 Attached Macros 内で取り上げられていた5種類のMacro、Peer Macro, Member Macro, Accessor Macro, Member Attribute Macro, Conformance Macroについて調査しまとめてみました。

これらのMacroについては、swift-macro-examples にて非常に分かりやすくまとまっていたのでぜひ手元で試してみることをオススメします。またMacroを理解する上、また実際に実装する上で swift-syntax についてざっくり理解する必要があるので、ドキュメントを読みつつ理解すると良いです。

Peer Macro

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

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

// 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 を定義することが可能です。(実際の実装は少々長いため省略します。)

// 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 に対して付与することで、以下の展開が行われます。

@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

Member Macro

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

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

// 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等々。詳しくは こちら)の配列を返し新たにメンバーを追加することが可能です。

例として swift-macro-examples から、Enumの各caseを判定するcomputed propertyを追加する CaseDetectionMacro は以下のように実装されています。

// 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情報にアクセスし、それを元に新たなメンバーの定義を追加することが可能です。

// 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定義がなされています。

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同様に classstruct 等に付加することで、その内部に持つプロパティや関数等に attribute を展開するマクロです。

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

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を用いて実装すると以下のようになります。

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定義がなされています。

// 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 を適合する等のユースケースがありそうです。

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

    return [("Equatable", nil)]
  }
}
24
15
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
15