こんにちは。新人プログラマーの岩本です。
今回はSwift5.9でリリースされた機能である「Macro」について、調べたことを解説していきます。
Swift Macroは、コードの再利用性を高めたり、冗長なコードを簡潔にする強力な機能です。これを理解することで、さらに効率的にコードを書けるようになります。
ぜひ最後までご覧ください。
この記事の想定読者
- Macroについて全く知らない人
- 気になるけど触ったことない人
Swift Macroとは
Swift MacroとはSwift5.9でリリースされた機能で、コンパイル時にコードを自動生成してくれるツールです。
コードの色々な箇所に出てくる処理やパターンをマクロとしてまとめることで、コードの再利用性や可読性を高めることができます。またボイラーコードを削減することもできるので、開発効率の向上にも繋がります。
またMacroになんらかのエラーがあった場合は、コンパイル時にエラーになるので、Macroの誤用の防止やバグの特定が容易になります。
Macroはコードを自動生成する、つまり常にコードを追加するだけなので、既存コードの変更は行いません。これにより安全にMacroを使えるようになってます。
2種類のMacro
Swift Macroには大きく分けて2種類あります。
- Freestanding Macro(自立型マクロ)
- Attached Macro(付属型マクロ)
順次解説します。
Freestanding Macro
Freestanding Macroは # で始まるマクロです。
公式のドキュメントではこのように説明されてます。
自立型マクロは、宣言に添付されることなく、それ自体独立して表示されます
簡単にいうとグローバル関数のように使えるMacroです。
具体例を挙げると、SwiftUIの #Preview
や、関数名を表示する #function
はこのFreestanding Macroに分類されます。
Freestanding Macroの中でも、役割に応じて種類が分類されているので、テーブルにしてまとめます。
名前 | 役割 |
---|---|
DeclarationMacro | 宣言マクロ。グローバルなブロックや、型宣言のブロックで利用できる。 |
ExpressionMacro | 式マクロ。マクロを式として利用できる。 |
CodeItemMacro | 関数のスコープ内で利用できるマクロ。 |
Attached Macro
Attached Macroは @ で始まるマクロです。
公式のドキュメントではこのように説明されています。
付属型マクロは、それが添付されている宣言を変更します。例えば、新しいメソッドを定義したり、プロトコルに準拠するコードを追加したりします。
構造体にプロパティを追加したり、プロパティに対してゲッター、セッターなどを自動生成できます。
具体例を挙げると、WWDC23で追加されたObservationや、Xcode16で追加されたEntryなどがこのAttached Macroに分類されます。
Attached Macroも同様に、役割に応じて種類が分類されています。
名前 | 役割 |
---|---|
AccessorMacro | 付加したプロパティのゲッター・セッターを自動生成する。 |
MemberMacro | actor, class, struct, enum, extension, protocolに対して付与できる。 付与したスコープ内でコードを自動生成する。 例えばstcurtに付加して、新しいプロパティを追加するなどの使い方ができる。 |
MemberAttributeMacro | 基本的にMemberMacroと同じ。 既存のプロパティや関数に対して属性を追加できる。 |
PeerMacro | メソッド、プロパティに対して付与できる。 付与したスコープ内でコードを自動生成する。 MemberMacroのプロパティ(メソッド)版なのかな? |
マクロの実装例
ではここからは、自分でマクロを実装していきます。
前準備としてXcodeを開き、「File」→「New」 →「Package」を選択してください。
次にSwift Macroを選択し、適当な名前をつけて作成してください。(今回はMacroSampleというパッケージ名をつけました)
※マクロの実装にはSwiftSyntaxの知識が必要ですが、今回は簡単な解説にとどめます。
Freestanding Macro
まずはFreestanding Macroの実装例を紹介します。
今回は引数として英文字列を渡して、小文字を大文字にして返すマクロ UppercaseLetterMacro
を実装します。
MacroSampleMacro.swiftに以下のコードを追加します。
public struct UppercaseLetterMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
// 引数リストから最初の引数を取得
guard let argument = node.arguments.first?.expression else {
fatalError("StringCheckマクロには1つの引数が必要です")
}
// 引数をStringにキャストする
guard let string = argument.as(StringLiteralExprSyntax.self) else {
fatalError("引数にはString型を入れてください")
}
return ExprSyntax(literal: string.representedLiteralValue?.uppercased())
}
}
詳しくコードを見てみます。
public struct UppercaseLetterMacro: ExpressionMacro {
UppercaseLetterMacroはExpressionMacroに準拠させています。
ExpressionMacroはFreestandingMacroの一種で、マクロを式として利用できます。
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
expansionメソッドはExpressionMacroに定義されているもので、マクロの展開を実装します。
nodeにはマクロの引数や呼び出し情報が入っており、contextにはコンパイラのコンテキスト情報が入っています。
// 引数リストから最初の引数を取得
guard let argument = node.arguments.first?.expression else {
fatalError("StringCheckマクロには1つの引数が必要です")
}
この部分でマクロに渡された最初の引数を取得しています。
引数が1つもなければエラーになります。
// 引数をStringにキャストする
guard let string = argument.as(StringLiteralExprSyntax.self) else {
fatalError("引数にはString型を入れてください")
}
return ExprSyntax(literal: string.representedLiteralValue?.uppercased())
この部分で取得した引数をString型にキャストして、大文字にして返しています。
次に MacroSamplePlugin
にUppercaseLetterMacro.selfを追加して、コンパイラが使えるようにします。
@main
struct MacroSamplePlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
StringifyMacro.self,
UppercaseLetterMacro.self
]
}
次に MacroSample.swift
に以下のコードを追加します。
@freestanding(expression)
public macro uppercaseLetter(_ value: String) -> String = #externalMacro(module: "MacroSampleMacros", type: "UppercaseLetterMacro")
この部分はマクロの宣言部分になります。
今回は式マクロとして定義し、externalMacro
で実装コードの場所を指定しています。
最後に main.swift
に以下のコードを追加します。
let hello = #uppercaseLetter("hello, world!")
print(hello) // HELLO, WORLD!
Terminalから swift run
を実行すると、HELLO, WORLD!と表示されることを確認できます。
Attached Macro
次にAttached Macroの実装例を紹介します。
今回は、付与したプロパティがnilであれば指定したデフォルトの値を返すようにするマクロ DefaultValueMacro
を実装します。
MacroSampleMacro.swiftに以下のコードを追加します。
public struct DefaultValueMacro: AccessorMacro {
public static func expansion(
of node: AttributeSyntax,
providingAccessorsOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AccessorDeclSyntax] {
// 引数リストを取得し、最初の引数と値をリテラルとして取得
guard let arguments = node.arguments?.as(LabeledExprListSyntax.self),
let firstArgument = arguments.first,
let valueExpr = firstArgument.expression.as(StringLiteralExprSyntax.self),
let defaultValue = valueExpr.representedLiteralValue else {
fatalError()
}
// 宣言から変数の初期化子を取得
guard let varDecl = declaration.as(VariableDeclSyntax.self),
let binding = varDecl.bindings.first,
let initializerValue = binding.initializer?.value else {
fatalError()
}
// 初期化値がnilでない場合、その値を返すgetアクセサを生成
if initializerValue.description != "nil" {
return [
AccessorDeclSyntax(stringLiteral: """
get { return \\(initializerValue) }
""")
]
}
// 初期化値がnilの場合、デフォルト値を返すgetアクセサを生成
return [
AccessorDeclSyntax(stringLiteral: """
get { \\"\\(defaultValue)\\" }
""")
]
}
}
詳しくコードを見ていきます。
public struct DefaultValueMacro: AccessorMacro {
DefaultValueMacroはAccessorMacroに準拠しています。
AccessorMacroはゲッター・セッターを生成するマクロです。
// 引数リストを取得し、最初の引数と値をリテラルとして取得
guard let arguments = node.arguments?.as(LabeledExprListSyntax.self),
let firstArgument = arguments.first,
let valueExpr = firstArgument.expression.as(StringLiteralExprSyntax.self),
let defaultValue = valueExpr.representedLiteralValue else {
fatalError()
}
この部分でマクロに渡された引数の値を取得しています。
LabeledExprListSyntaxはラベル付きの引数を解析する際に使われるものです。
例えば @DefaultValue("Hello")
と定義された場合、Helloという文字列を取得できます。
// 宣言から変数の初期化子を取得
guard let varDecl = declaration.as(VariableDeclSyntax.self),
let binding = varDecl.bindings.first,
let initializerValue = binding.initializer?.value else {
fatalError()
}
この部分で付与した変数の初期値を取得しています。
VariableDeclSyntaxは変数や定数の解析をする際に使われるものです。
例えば以下のようなコードの場合、
@DefaultValue("Hello")
var value: String? = "AAA"
AAAという文字列を取得できます。
// 初期化値がnilでない場合、その値を返すgetアクセサを生成
if initializerValue.description != "nil" {
return [
AccessorDeclSyntax(stringLiteral: """
get { return \\(initializerValue) }
""")
]
}
// 初期化値がnilの場合、デフォルト値を返すgetアクセサを生成
return [
AccessorDeclSyntax(stringLiteral: """
get { \\"\\(defaultValue)\\" }
""")
]
この部分でゲッター節をreturnしています。
初期値がnilじゃない場合はそのまま値を返し、nilの場合は指定した上で取得したデフォルトの値を返すようにしています。
次に MacroSamplePlugin
にUppercaseLetterMacro.selfを追加します。
@main
struct MacroSamplePlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
UppercaseLetterMacro.self,
DefaultValueMacro.self
]
}
次に MacroSample.swift
に以下のコードを追加します。
@attached(accessor)
public macro DefaultValue(defaultValue: String) = #externalMacro(module: "MacroSampleMacros", type: "DefaultValueMacro")
今回は引数はString型に固定してます。
最後に main.swift
に以下のコードを追加します。
struct Hoge {
@DefaultValue(defaultValue: "AAAA!")
var value1: String? = nil
@DefaultValue(defaultValue: "これは表示されないよ!")
var value2: String? = "Hello, World!"
}
let hoge = Hoge()
print(hoge.value1) // AAAA!
print(hoge.value2) // Hello, World!
コードを実行すると、「AAAA!」と「Hello, World!」が表示されることを確認できます。
まとめ
今回はSwift Macroについて解説しました。
Macroを使いこなせるようになると、開発効率がグッと高まると思います。
ぜひご自身の手で色々と試してみてください。
ここまでのご閲覧ありがとうございました!
コメント