4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftAdvent Calendar 2024

Day 6

[Swift] デフォルト引数を持つオーバーロードをProtocol Extensionに生成するMacro

Last updated at Posted at 2024-12-08

背景

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使わず普通に書けば良いというのはあるのであくまでマクロ実装の参考までに、マクロは実装コストと受けられる恩恵のコスパ判断が難しいですね。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?