Member Macro Conformances
01 何が問題だったのか
SE-0402 で conformance ロールが extension ロールに一般化された結果、プロトコル適合に関わるメンバーは基本的にエクステンション側に生やすのがよい、という整理になりました。エクステンションに寄せておけば、たとえばイニシャライザを追加しても struct の memberwise initializer が抑制されない、といった利点もあります。
extension ロールにはもうひとつ重要な仕組みとして、まだ付属先の型が適合していないプロトコル だけを conformingTo: で受け取れるようになっており、すでに(スーパークラス経由などで)適合済みのプロトコルに対して二重に実装を生やさない判断ができます。
しかし、適合に必要なメンバーをどうしてもプライマリな型定義の側に置きたい ケースが残ります。代表的なのは次のような場合です。
- 非
finalなクラスでプロトコル要件を満たすイニシャライザを書く場合。継承可能にするためにrequired initでなければならず、これはエクステンションには書けません。 - 非
finalなクラスでオーバーライド可能なメンバーを提供したい場合。 - stored property や
enumの case など、そもそもエクステンションに書けない宣言で要件を満たす場合。
これらは member ロールで生やすことになりますが、member ロールには「どのプロトコルへの適合のために生やすのか」「どのプロトコルにはすでに適合済みなのか」という情報が一切渡されません。結果として、たとえば Codable を自動実装するマクロを書こうとすると、スーパークラスが既に Encodable に適合しているケースで init(from:) / encode(to:) を二重に生やしてしまう、といった不整合が避けられませんでした。Encodable / Decodable のように「エクステンションに書けないメンバー」が要件に含まれる適合を自動化するマクロは、この制約のせいで実質的に実装不能でした。
02 どのように解決されるのか
@attached(member, ...) に、extension ロールと同じように conformances: を指定できるようにし、マクロ実装の expansion には まだ適合していないプロトコルの一覧 を conformingTo: で渡します。これにより、member マクロも「どの適合のためにメンバーを生やすべきか」を判断できるようになります。
conformances: に指定できるのは、プロトコル名・プロトコル合成(A & B)・それらを指す typealias で、SE-0402 の extension ロールと同じ仕様です。member ロール自体は適合そのものを宣言できません(適合を宣言できるのは引き続き extension ロールだけです)。member ロールの役割はあくまで、stored property や required init のように「エクステンションに書けないメンバー」を必要に応じて供給することです。
Codable マクロの例
典型的な使い方は、member ロールと extension ロールを同じマクロに重ねる構成です。Codable の自動実装を提供するマクロは次のように宣言できます。
@attached(member, conformances: Decodable, Encodable, names: named(init(from:), encode(to:)))
@attached(extension, conformances: Decodable, Encodable, names: named(init(from:), encode(to:)))
macro Codable() = #externalMacro(module: "MyMacros", type: "CodableMacro")
付属先の型に応じて、マクロ実装は次のように振る舞いを切り替えられます。
struct/enum/ アクター /finalなクラスでは、init(from:)とencode(to:)を エクステンション側 に生やします。structではこれにより memberwise initializer が残ります。- 非
finalなクラスでは、サブクラスからオーバーライドできるよう、init(from:)とencode(to:)を プライマリな型定義側 に(memberロールから)生やします。 - スーパークラスから
Encodable/Decodableを継承しているクラスでは、init(from:)/encode(to:)の中でスーパークラスの実装を呼び出し、クラス階層全体をまとめて decode/encode します。この判断は、conformingTo:に渡ってくるのが[Decodable]だけ(Encodableは既に適合済みのため外される)といった形で拾えます。
マクロ実装側のシグネチャ
MemberMacro プロトコルの expansion に conformingTo: 引数が追加されます。
protocol MemberMacro: AttachedMacro {
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax]
}
conformingTo: に渡される配列は、@attached(member, conformances: ...) に挙げたプロトコルのうち、付属先の型がまだ明示的に適合していないもの だけを並べたものです。スーパークラスや implied conformance 経由で既に適合しているプロトコルはここから取り除かれるため、extension ロールと同じ要領で「どの適合のためにメンバーを生やすか」を絞り込めます。
なお、member ロールはあくまでメンバーを供給するロールなので、返した [DeclSyntax] に適合を書き加えることはできません。適合そのものは extension ロール側で宣言する、という役割分担はそのままです。