背景
Swift 6.0現在ではprotocolのメソッドに直接デフォルト引数を持たせることができません。
protocol ItemRepositoryProtocol {
// 🔴 Default argument not permitted in a protocol method
func getItems(pageSize: Int = 20, pageToken: String? = nil) async throws -> [Item]
}
迂回手法としてextensionにデフォルト引数を持つ定義を書くことができます。
extension ItemRepositoryProtocol {
func getItems(pageSize: Int = 20, pageToken: String? = nil) async throws -> [Item] {
try await getItems(pageSize: pageSize, pageToken: pageToken)
}
}
DefaultArgumentMacro
上記パターンを自動的に生成するmacroです。
使用例
import DefaultArgument
struct Item {}
enum SortKind {
case name
case like
}
@DefaultArgument(funcName: "getUseritems", defaultValues: ["sortKind": SortKind.name, "pageSize": 20, "pageToken": nil])
@DefaultArgument(funcName: "getItems", defaultValues: ["sortKind": SortKind.name, "pageSize": 20, "pageToken": nil])
protocol ItemRepositoryProtocol {
func getItem(id: String) async throws -> Item
func getItems(sortKind: SortKind, pageSize: Int, pageToken: String?) async throws -> [Item]
func getUseritems(userID: String, sortKind: SortKind, pageSize: Int, pageToken: String?) async throws -> [Item]
}
struct ItemRepository: ItemRepositoryProtocol {
func getItem(id: String) async throws -> Item {
.init()
}
func getItems(sortKind: SortKind, pageSize: Int, pageToken: String?) async -> [Item] {
[]
}
func getUseritems(userID: String, sortKind: SortKind, pageSize: Int, pageToken: String?) async throws -> [Item] {
[]
}
}
do {
let itemRepository: any ItemRepositoryProtocol = ItemRepository()
// マクロが生成したデフォルト引数ありの関数を呼ぶ
_ = try await itemRepository.getItems(pageSize: 10)
_ = try await itemRepository.getUseritems(userID: "1234")
} catch {
}
実装方法
Attached macroのextension macroで実装。重要なところだけ軽く説明。100行程度なので是非実際のソースコードを読んでみてください。
関数呼び出しを生成するところ。めんどくさいので完全にSyntaxBuilderに任せている
// add functionCallExpr
do {
let parameters = targetFuncDecl.signature.parameterClause.parameters
let callParameters = parameters.map { parameter in
let parameterLabel = parameter.firstName.text
return "\(parameterLabel): \(parameterLabel)"
}.joined(separator: ", ")
var callBase = "\(funcName)(\(callParameters))"
if targetFuncDecl.isAsyncFunc {
callBase = "await " + callBase
}
if targetFuncDecl.isThrowsFunc {
callBase = "try " + callBase
}
targetFuncDecl.body = CodeBlockSyntax(stringLiteral: "{\(callBase)}")
}
デフォルト引数を関数定義 (FunctionDecl
) に追加するところ
// macroの引数で指定したDictionaryを取り出す
let defaults: [String: ExprSyntax] = ...
for (argName, defaultValue) in defaults {
guard let index = targetFuncDecl.signature.parameterClause.parameters.firstIndex(where: { $0.firstName.text == argName }) else {
throw DefaultArgumentMacroError.argNameNotFound
}
// FunctionParameter.defaultValue(デフォルト値のnode)にmacroの引数で指定したデフォルト値を入れる
targetFuncDecl.signature.parameterClause.parameters[index].defaultValue = InitializerClauseSyntax(value: defaultValue)
}
感想
まあこれくらいならmacro使わず普通に書けば良いというのはあるのであくまでマクロ実装の参考までに、マクロは実装コストと受けられる恩恵のコスパ判断が難しいですね。