以前書いた ↑ 記事の中で『タプルをEquatable、ComparableやHashableに準拠させる』方法として、次のstruct
を定義する方法を提案しました。
struct Tuple {
typealias Element = (x: Int, y: Int) //カスタマイズする
let tuple: Element
init(_ _tuple: Element) { self.tuple = _tuple }
func callAsFunction() -> Element { self.tuple }
}
extension Tuple: Equatable {
static func == (lhs: Tuple, rhs: Tuple) -> Bool {
lhs.tuple == rhs.tuple
}
}
extension Tuple: Comparable {
static func < (lhs: Tuple, rhs: Tuple) -> Bool {
lhs.tuple < rhs.tuple
}
}
extension Tuple: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(tuple.x)
hasher.combine(tuple.y)
}
}
extension Tuple: CustomStringConvertible {
//おまけで`CustomStringConvertible`にも準拠しておく
var description: String {
"\(type(of: self))(\(tuple.x), \(tuple.y))"
}
}
上のコードを雛形
として、2行目のtypealias Element
を、実際に使うタプルに変更する必要がありました。
そこで、今回は、雛形を手修正するのではなく、実際に使用するタプルに沿ったstruct
を自動生成する Swiftマクロ を作ってみました。
以下のソースコード名は、Xcode で New → Package → Swift Macro にて、
MyMicro
という名前で新規パッケージプロジェクトを作った前提です。
マクロ エンドポイント定義
マクロのエンドポイントを定義します。
- 生成するstruct名 :
HashableTuple
- マクロ名 :
GenarateHashableTuple
(#GenarateHashableTuple
) - マクロの実装 :
HashableTupleMacro
とします。
@freestanding(declaration, names: named(HashableTuple))
public macro GenarateHashableTuple<T>(_ value: T) = #externalMacro(module: "MyMacroMacros", type: "HashableTupleMacro")
マクロの実装
@main
struct MyMacroPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
HashableTupleMacro.self,
]
}
public struct HashableTupleMacro: DeclarationMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let argument = node.argumentList.first?.expression,
let tupleExpr = argument.as(TupleExprSyntax.self)
else {
fatalError("compiler bug: the macro does not have any arguments")
}
let count = tupleExpr.elements.count
let sp8 = String(repeating: " ", count: 8)
var hasher = ""
for index in 0 ..< count {
if !hasher.isEmpty { hasher += "\n" }
hasher += sp8 + "hasher.combine(tuple.\(index))"
}
return [
"""
struct HashableTuple: Equatable, Comparable, Hashable, CustomStringConvertible {
typealias Element = \(argument)
let tuple: Element
init(_ _tuple: Element) { self.tuple = _tuple }
func callAsFunction() -> Element { self.tuple }
//Equatable
static func == (lhs: HashableTuple, rhs: HashableTuple) -> Bool {
lhs.tuple == rhs.tuple
}
//Comparable
static func < (lhs: HashableTuple, rhs: HashableTuple) -> Bool {
lhs.tuple < rhs.tuple
}
//Hashable
func hash(into hasher: inout Hasher) {
\(raw: hasher)
}
//CustomStringConvertible
var description: String {
"\\(type(of: self))\\(self())"
}
}
""",
]
}
}
使用例
#GenarateHashableTuple
マクロの引数に 実際に使用するタプル型 を指定します。
タプルの要素に指定できる
"型"
は、Equatable
,Comparable
,Hashable
に準拠している必要がある
#GenarateHashableTuple((x: Int, y: Int, s: String))
let tuple = HashableTuple((10, 20, "str"))
print(tuple)
//HashableTuple(x: 10, y: 20, s: "str")
let (x, y, s) = tuple()
print((x, y, s))
//(10, 20, "str")
生成された struct のコードを、次に示します。
#GenarateHashableTuple((x: Int, y: Int, s: String))
の行で、マウス右クリック →「Expand macro」を選択すると、生成されたコードが表示される。
//#GenarateHashableTuple((x: Int, y: Int, s: String))
struct HashableTuple: Equatable, Comparable, Hashable, CustomStringConvertible {
typealias Element = (x: Int, y: Int, s: String)
let tuple: Element
init(_ _tuple: Element) {
self.tuple = _tuple
}
func callAsFunction() -> Element {
self.tuple
}
//Equatable
static func == (lhs: HashableTuple, rhs: HashableTuple) -> Bool {
lhs.tuple == rhs.tuple
}
//Comparable
static func < (lhs: HashableTuple, rhs: HashableTuple) -> Bool {
lhs.tuple < rhs.tuple
}
//Hashable
func hash(into hasher: inout Hasher) {
hasher.combine(tuple.0)
hasher.combine(tuple.1)
hasher.combine(tuple.2)
}
//CustomStringConvertible
var description: String {
"\(type(of: self))\(self())"
}
}
#GenarateHashableTuple
の引数に書いたタプルがtypealias Element
とhash
関数に反映されています。
extension
をマクロで生成するとエラーになるため、extension
を使っていません。
エラー:”swift Macro expansion cannot introduce extension”
このタプル(struct)を Set型
の要素や、Dictonary型
のkey
として使うことができます。sort
/sorted
も比較関数不要でソートできます。
大変便利ですね。
ただし、独自マクロのため、#GenarateHashableTuple
マクロを 競プロ で使うことはできません。ローカルに生成/展開したコードをコピペして使ってください。
ここで作成したマクロを実際にパッケージ化して、他のSwiftプロジェクトで SwiftPM を使ってインポートする方法は別記事に譲ります。
以上