Swift Digest
SE-0415 | Swift Evolution

Function Body Macros

Proposal
SE-0415
Authors
Doug Gregor
Review Manager
Tony Allevato
Status
Implemented (Swift 6.0)

01 何が問題だったのか

SE-0389 で導入された attached macro は、peer / member / accessor / memberAttribute など、宣言の周辺にコードを足すためのロールを備えていました。しかし、関数の本体そのもの を合成したり書き換えたりするロールは用意されていませんでした。

そのため、次のような「関数の本体を作る・包む・置き換える」という用途はマクロでは実現できませんでした。

  • 宣言とメタデータから本文を組み立てる。たとえば、引数をまとめてリモート呼び出しに渡す実装を自動生成したい。
  • 既存の本文にコードを足す。関数の出入りでログやトレースを出す、事前条件を確認する、不変条件を確立するなど。
  • 既存の本文を包む。MainActor.assumeIsolated { ... }withSpan("...") { ... } のようなラッパーに本文を入れ直す。
  • 本文を別の表現として解釈し直す。たとえば、本文をドメイン固有言語として受け取り、実行可能コードに「lower」する。

これらの用途は、peer マクロで関数を新しく生やすような回避策では自然に書けず、attached macro に新しいロールを追加する必要がありました。

02 どのように解決されるのか

attached macro に新しく body ロールを追加し、関数・イニシャライザ・デイニシャライザ・アクセサの本文をマクロで生成・置き換えできるようにします。マクロ宣言には @attached(body) を付けます。

@attached(body) macro Remote() = #externalMacro(...)
@attached(body) macro Logged() = #externalMacro(...)
@attached(body) macro AssumeMainActor() = #externalMacro(...)

他の attached macro と同じく、body マクロには戻り値型はありません。

使い方のパターン

本文を丸ごと合成する例として、引数をそのままリモート呼び出しに渡す @Remote が考えられます。

@Remote
func f(a: Int, b: String) async throws -> String

この宣言は、たとえば次のように展開されます。

func f(a: Int, b: String) async throws -> String {
    return try await remoteCall(function: "f", arguments: ["a": a, "b": b])
}

既存の本文をそのまま残して前後にコードを足す例が @Logged です。

@Logged
func g(a: Int, b: Int) -> Int {
  return a + b
}

は、次のように展開できます。

func g(a: Int, b: Int) -> Int {
  log("Entering g(a: \(a), b: \(b))")
  defer {
    log("Exiting g")
  }
  return a + b
}

既存の本文をラップして置き換える例が @AssumeMainActor です。@MainActor を付けられない nonisolated 関数の中身を MainActor.assumeIsolated { ... } に入れ直す、というよくあるパターンをマクロとして抽象化できます。

extension MyView: SomeDelegate {
  @AssumeMainActor
  nonisolated func onSomethingHappened(event: Event) {
    myView.title = newTitle(processing: event)
  }
}

は次のように展開されます。

extension MyView: SomeDelegate {
  nonisolated func onSomethingHappened(event: Event) {
    MainActor.assumeIsolated {
      myView.title = newTitle(processing: event)
    }
  }
}

アクセサとプロパティ短縮構文

body マクロはアクセサにも付けられます。その場合はアクセサそのものに付けます。

var area: Double {
  @Logged get {
    return length * width
  }
}

get-only プロパティの短縮構文を使うときは、プロパティ自体に付けることもできます。

@Logged var area: Double {
  return length * width
}

実装側: BodyMacro プロトコル

body マクロの実装は BodyMacro プロトコルに適合する型として書きます。expansion は、付属先の宣言と属性の構文ノードを受け取り、新しい本文となる [CodeBlockItemSyntax] を返します。

public protocol BodyMacro: AttachedMacro {
  static func expansion(
    of node: AttributeSyntax,
    providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax,
    in context: some MacroExpansionContext
  ) throws -> [CodeBlockItemSyntax]
}

付属先が既に本文を持っていた場合、その本文は暗黙に無視されexpansion が返したコードで置き換わります。元の本文を活かしたければ、マクロ実装側で declaration の構文から取り出して組み込みます。

合成の制約

body マクロはひとつの関数に最大1つ しか付けられません。複数の body マクロを合成することはできず、必要なら片方のマクロ実装内で、もう片方に相当する展開も行う形になります。

他のロール(peer など)との併用は可能で、各ロールの expansion はそれぞれ独立に元の宣言を受け取ります。

元の本文は型検査されない

body マクロが付いた関数について、ユーザーが書いた本文は syntactic well-formed(Swiftの文法に合致している)ことだけが要求され、型検査は行われません。マクロ展開後の新しい本文だけが型検査の対象になります。

これは他の attached macro と同じ方針で、マクロ実装は「型検査前の素の構文木」を受け取ります。この仕組みによって、たとえば関数本文をSQL文として解釈して書き換えるような @SQL マクロも原理的には書けます。

@SQL
func employees(hiredIn year: Int) -> [String] {
  SELECT
    name
  FROM
    employees
  WHERE
    YEAR(hire_date) = year;
}

ただし、あくまでもSwiftの文法として通る範囲 に限られます。Swiftの文として解釈できない書き方、たとえば1行に複数文が並ぶような形はパースの時点でエラーになります。

@SQL
func employees(hiredIn year: Int) -> [String] {
  SELECT name FROM employees        // error: consecutive statements on a line must be separated by ';'
    WHERE YEAR(hire_date) = year;
}

この「文法としては通る必要がある」という制約が、body マクロの過度な濫用を抑えつつ、既存のソースコードツール(フォーマッタやエディタ)が壊れずに動くことも助けています。

Future Directions(speculative)

将来の拡張として次の方向性が挙げられています。いずれも今回のProposalの範囲外で、実現を約束するものではありません。

  • クロージャへの適用: 現状の body マクロは宣言された関数・イニシャライザ・デイニシャライザ・アクセサに限定されています。クロージャにも付けられるよう拡張する方向が検討されていますが、クロージャは式の中に現れるため、マクロ引数とまわりの型推論との相互作用という難しさがあります。
  • preamble マクロ: 関数の先頭だけに共通コードを差し込むロール。初期レビューでは含まれていましたが、合成可能性や診断・補完のしやすさといった利点はあるものの、body マクロがあればほぼ表現できることから Future Directions に回されました。
  • wrapper マクロ: withSpan("...") { ... } のように本文をクロージャとして包むパターン専用のロール。元の本文を関数値として型検査した上でマクロに渡せるので、body マクロのデメリット(本文が型検査されない)を回避でき、複数適用による合成もしやすい、という利点が示されています。Pythonのデコレータ風のスタイルに近いものです。