Swiftマクロの種類とその作用範囲
はじめに
Swift 5.9でマクロ機能がリリースされました。
マクロを使えば、機械的な実装を自動化したりまったく新しい機能を作れたりします。マクロにはソースコードを無秩序に書き換えるイメージがあるかもしれませんが、Swiftのマクロは厳密に型定義されており、自由度が高くありつつ秩序を守れる仕組みとして導入されています。
Swiftのマクロには大きくFreestanding MacroとAttached Macroの2種類があり、それぞれが更に何種類か細部化されています。
これらの種類は、マクロが利用できる場所と、その作用範囲が異なっています。
本記事ではこれらマクロについて、利用できる箇所と作用できる範囲についてまとめたいと思います。
また、マクロの宣言時にはnames
パラメータが必要なものがあり、これについても解説します。
資料
環境
$ swift --version
swift-driver version: 1.87.1 Apple Swift version 5.9 (swiftlang-5.9.0.128.108 clang-1500.0.40.1)
Target: arm64-apple-macosx13.0
Freestanding Macro
Freestanding Macroは、#
で始まることが特徴です。
自由な場所に記述でき、引数として受け取ったパラメータを使うことができます。逆にいえばマクロの引数以外の情報は利用できません。
コード中のブロックごとに利用できるマクロは異なり、ブロックに合った文法を展開できます。
DeclarationMacro
宣言マクロです。グローバルなブロックや、型宣言のブロックで利用できます。
structなどの型宣言や、varなどのプロパティ宣言、関数宣言を出力できます。
struct Student {
#PhantomID(UUID.self)
}
struct Student {
struct ID: RawRepresentable, Hashable, Sendable {
init?(rawValue: UUID) {
self.rawValue = rawValue
}
var rawValue: UUID
}
var id: ID
}
注意点として、関数スコープ内でこのマクロを利用した場合は専用にスコープが区切られて展開されます。
つまり、上の例のマクロは次のようなイメージで展開されます。
func foo() {
#PhantomID(UUID.self)
let value = ID(rawValue: UUID())
}
func foo() {
do {
struct ID: RawRepresentable, Hashable, Sendable {
init?(rawValue: UUID) {
self.rawValue = rawValue
}
var rawValue: UUID
}
}
let value = ID(rawValue: UUID()) // IDは見つからずエラーになる
}
専用スコープが切られているため、マクロで生成した型や変数、deferをうまく使うことは難しいです。
ExpressionMacro
式マクロです。関数ブロック内で式として利用できます。
1つの式を出力できます。
イメージとしては普通のグローバル関数にかなり近いです。
func testFoo() {
let x: Double = 1.0
let y: Int = 42
let result = #implicitCast(x * y)
}
func testFoo() {
let x: Double = 1.0
let y: Int = 42
let result = (Double(x) * Double(y))
}
swift-power-assertはこのマクロです。
CodeItemMacro
関数ブロック内で利用できます。
関数ブロックで記述できるあらゆるコードを複数出力でき、インナータイプやguard文も出力できて表現力が高いです。
しかしこのマクロはSwift 5.10時点ではexperimentalのようです。開発中の6.0も同様で正式なプロポーザルもなく、状況がよくわかりません。
button.onTap = { [weak self] in
#guard(self)
}
button.onTap = { [weak self] in
guard let self else { return }
}
Attached Macro
Attached Macroは、@
で始まることが特徴です。
何らかの宣言に対して付与します。
Freestanding Macroと違って自由な場所に記述することはできませんが、付与された対象の型定義や変数定義をマクロ内で活用でき、既存コードを拡張する方向で力を発揮します。
AccessorMacro
プロパティ宣言に対して付与でき、getterやsetterなどアクセサースコープ要素を出力できます。
プロパティに処理を割り込んだり値の保存先を変えるなど、Property Wrapperのようなことができます。
get
とset
を定義すれば値の保存先を変えることができますが、代わりの保存先は用意する必要があります。保存先を追加することはAccessorMacroだけではできませんが、後述するPeerMacroやMemberMacroと組み合わせることで行なえます。
@Trace
var value: Int
var value: Int {
willSet {
print(newValue)
}
}
MemberMacro
様々な型宣言に付与でき、そのスコープ内に別の宣言を出力できます。
付与された型に存在する多くの情報を利用できるため、自由度が高いです。
@CaseDetection
enum Animal {
case dog
case cat(curious: Bool)
}
enum Animal {
case dog
case cat(curious: Bool)
var isDog: Bool {
if case .dog = self {
return true
}
return false
}
var isCat: Bool {
if case .cat = self {
return true
}
return false
}
}
MemberAttributeMacro
基本は上述したMemberMacroと同じですが、こちらは既存の変数や関数に対して追加のattributeを出力するためのものです。
この際に別のマクロも付与でき、組み合わせて使うことができます(マクロは複数回展開されるため、正しく動作します)。
Swiftのマクロは基本的にコードを追加するのみで、既存コードを編集することができません。
attributeの追加はMemberMacroではできないため、こちらが用意されています。
@memberDeprecated
struct SomeStruct {
typealias MacroName = String
var oldProperty: Int = 420
func oldMethod() {
print("This is an old method.")
}
}
struct SomeStruct {
@available(*, deprecated)
typealias MacroName = String
@available(*, deprecated)
var oldProperty: Int = 420
@available(*, deprecated)
func oldMethod() {
print("This is an old method.")
}
}
PeerMacro
様々な宣言に対して付与でき、その宣言と同じスコープに対して別の宣言を出力できます。
protocol定義から具体型を出力したり、変数に対する独自のsetter関数を定義するなど用途は多岐に渡ります。
MemberAttributeMacroと組み合わせて使われることも多いと思います。
@AddAsync
func send(request: Request, completion: @escaping @Sendable (Result<String, Error>) -> Void) {
completion(.success("OK"))
}
func send(request: Request, completion: @escaping @Sendable (Result<String, Error>) -> Void) {
completion(.success("OK"))
}
func send(request: Request) async throws {
try await withCheckedThrowingContinuation { continuation in
send(request: request) { result in
switch result {
case .success(let value):
continuation.resume(returning: value)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
ExtensionMacro
型宣言に対して付与でき、新たなextensionを出力できます。
前述したPeerMacroの、extension専用のものです。PeerMacroでextensionを生やそうとすると後述するnames
パラメータの成約をうまく満たすことができないため、専用のものが用意されています。
@equatable
final class Message {
let text: String
}
final class Message {
let text: String
}
extension Message: Equatable {
}
注意点として、extensionはマクロを付与した型のみにしか行なえません。マクロによる拡張がコンパイラが予測可能な範囲に抑えられています。
@TypeName
struct Message {}
struct Message {}
// ⚠️Message以外に対してextensionを追加することはできない
extension String {
static var Message: String {
return "\(Message.self)"
}
}
ConformanceMacro
種類だけ定義されていますが、まだ実装がありません。
プロポーザルには記載があり、型定義を見る限りでは各種型宣言に対して利用でき、特定のprotocolに適合させることができそうです。
ExtensionMacroと役割が被りそうにも思いますが、詳細は調べていません。
追記
ExtensionMacroのプロポーザルによると、ExtensionMacroはConformanceMacroより拡張性の高い上位の存在で、ConformanceMacroは不要となったようです。そのために実装されていないのだと思います。
マクロ宣言時の追加パラメータについて
Swiftのマクロには新しいIdentifier(型、変数、関数など)を出力する際はそれを明示的にしなければならない制約があります。
出力するIdentifierの命名規則をマクロ宣言に記載する必要があります。
登録されていないものが出力された場合はコンパイルエラーになります。
names
DeclarationMacro、MemberMacro、PeerMacro、ExtensionMacroで新しい型や変数、関数を生やす際に必要です。
@freestanding(declaration, names: named(ID), named(id))
public macro PhantomID<T: Hashable & Sendable>(_: T.Type) = #externalMacro(...)
names:
の値としては以下の5種類の値が使えます。
値 | 意味 |
---|---|
named(name) |
固定の名前(name ) |
prefixed(prefix) |
元の名前に接頭辞を付与した名前(元がFooの場合、prefixFoo ) |
suffixed(suffix) |
元の名前に接尾辞を付与した名前(元がFooの場合、Foosuffix ) |
overloaded |
元と同じ名前でオーバーロードする |
arbitrary |
自由な名前。これはグローバルスコープでは使用不可 |
arbitrary
は名前を事前に宣言するルールを破ることができますが、グローバルなスコープにこれで出力するとエラーになるという制約があります。
もし自由な名前で何かを宣言したい場合、一度別の名前空間をsuffixed(suffix)
で生やすなどして、その中に宣言する、などのテクニックが必要となります。
conformances
ExtensionMacroで必要です。
上述したnames:
とほぼ同じで、conformする対象のprotocolをconformances:
で指定が必要になります。
@attached(extension, conformances: Equatable)
public macro equatable() = #externalMacro(module: "MacroExamplesImplementation", type: "EquatableExtensionMacro")
おまけ
「宣言」の文法とは
SwiftSyntaxのコードを見る限り、以下のようです。
- 各種型宣言
actor A {}
class C {}
struct S {}
enum E {}
protocol P {}
macro M() = ...
- accociatedtype宣言
protocol P {
accociatedtype A // ← これ
}
- case宣言
enum E {
case foo // ← これ
}
- 各種関数宣言とプロパティ宣言
class C {
func f() {}
deinit {}
subscript(_ i: Int) {}
var value: Int
}
- マクロ利用宣言
Freestanding Macroの利用も宣言となっていました。
#assert()
- その他
import Foundation // ← これ
extension S {} // ← これ
typealias A = ... // ← これ
他にもオペレータ関係の普段使わないものが含まれていますが、割愛します。
Discussion