Swift Digest
SE-0402 | Swift Evolution

Generalize conformance macros as extension macros

Proposal
SE-0402
Authors
Holly Borla
Review Manager
John McCall
Status
Implemented (Swift 5.9)

01 何が問題だったのか

SE-0389 で導入された attached macro には、conformance ロールがありました。これは付属先の型に対してエクステンションを生やし、そこにプロトコル適合を追加するためのロールです。

@attached(conformance)
macro AddEquatable() = #externalMacro(...)

@AddEquatable
struct S {}

// 展開後
extension S: Equatable {}

しかし conformance ロールは単体ではあまりに非力でした。返せるのは「プロトコル名」と「where 句」だけで、適合に必要なメンバーは別途 member ロールで生やす必要がありました。

より本質的な問題は、マクロが付属先の型のエクステンションを生やす唯一の手段が conformance ロールだった という点です。メンバーをエクステンション側に置けないことは、マクロシステムにとって大きな制約です。エクステンションには次のような、プライマリな型定義に直接書いた場合とは異なる意味論があるためです。

  • プロトコルの要件に対するデフォルト実装は、プロトコルのエクステンションにしか書けません。
  • エクステンションに追加したイニシャライザは、コンパイラが自動合成するイニシャライザを抑止しません。
  • プロトコルやクラスのエクステンションに書いた computed property やメソッドは、dynamic dispatch の対象になりません。

加えて、スタイル上も、エクステンションにまとめて書いた方がジェネリック要件を一度に指定できるなどの利点があり、「適合の実装はエクステンション側に書く」という慣習と揃えるためにもエクステンションを生やせる仕組みが必要でした。

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

conformance ロールを廃し、より一般化された extension ロールを導入します。extension ロールのマクロは、付属先の型に対するエクステンションを生やし、その中にプロトコル適合・where 句・メンバー定義をまとめて書き出せます。

protocol MyProtocol {
  func requirement()
}

@attached(extension, conformances: MyProtocol, names: named(requirement))
macro MyProtocol = #externalMacro(...)

@MyProtocol
struct S<T> {}

// 展開後
extension S: MyProtocol where T: MyProtocol {
  func requirement() { ... }
}

生やせるのは 付属先の型そのもののエクステンション に限られます。また、追加するプロトコル適合とメンバー名は、SE-0389 の原則に従ってあらかじめ申告しておく必要があります。

申告する内容

@attached(extension, ...) には次の二つを指定します。

  • conformances: — エクステンションの継承句に並べるプロトコル(適合制約)。ここに挙げたプロトコル以外に適合を追加することはできません。適合制約としては、プロトコル名のほか、プロトコル合成(A & B)や、それらを指す typealias(例: typealias Codable = Encodable & Decodable)も指定できます。
  • names: — エクステンションの内側に生やすメンバー名。named(...)prefixed(...)suffixed(...)arbitrary が使えます。ここに含まれない名前は生やせません。

さらに、extension ロールが生やしたエクステンションに peer マクロが付いている状態は、peer の展開結果の名前が元の @attached(extension) では申告できないためエラーになります。

適用できる場所

extension ロールのマクロは 名義型(nominal type)のプライマリ宣言にのみ 付けられます。typealias や既存の extension 宣言には付けられません。

一方で、Swift ではエクステンションをトップレベルにしか書けないという制約があるため、ネストした型に適用した場合は 展開結果のエクステンションがファイルのトップレベルに挿入されます

@attached(extension, conformances: MyProtocol, names: named(requirement))
macro MyProtocol = #externalMacro(...)

struct Outer {
  @MyProtocol
  struct Inner {}
}

// 展開後
struct Outer {
  struct Inner {}
}

extension Outer.Inner: MyProtocol {
  func requirement() { ... }
}

関数の内側などに書いたローカル型には、そもそもエクステンションが書けないため、extension ロールのマクロは付けられません。

マクロ実装

extension ロールのマクロ実装は ExtensionMacro プロトコルに適合させ、ExtensionDeclSyntax の配列を返します。

public protocol ExtensionMacro: AttachedMacro {
  static func expansion(
    of node: AttributeSyntax,
    attachedTo declaration: some DeclGroupSyntax,
    providingExtensionsOf type: some TypeSyntaxProtocol,
    conformingTo protocols: [TypeSyntax],
    in context: some MacroExpansionContext
  ) throws -> [ExtensionDeclSyntax]
}

返すエクステンションは、providingExtensionsOf に渡される(ネスト型であれば Outer.Inner のように完全修飾された)型を対象としなければなりません。

conformingTo: パラメータは、まだ付属先の型が適合していないプロトコル だけを並べた配列です。これはマクロ実装側で「すでに書かれている適合を二重に宣言しない」判断を行えるようにするためのものです。元のソースで明示的に書かれている適合はもちろん、継承や implied conformance によって既に満たされている適合もここから除かれます。

たとえば次のコードでは、S は既に Encodable に適合しているため、conformances: Codable(= Encodable & Decodable)を申告していても、マクロの expansion に渡される conformingTo:[Decodable] だけになります。マクロ実装はこれを見て、Decodable の適合だけを追加するエクステンションを返せます。

typealias Codable = Encodable & Decodable

@attached(extension, conformances: Codable)
macro MyMacro() = #externalMacro(...)

@MyMacro
struct S { ... }

extension S: Encodable { ... }

SE-0389 との関係

SE-0389 は Swift 5.9 時点で実装済みですが、本提案は同じ Swift 5.9 のリリースまでに取り込まれました。そのため conformance ロールは最終的に言語から取り除かれます。仮に本提案が 5.9 以降にずれ込んだ場合でも、conformance ロールは「適合だけを追加する extension ロールの糖衣」として残す方針が示されています。