[Swift] HashableTuple を生成する Swift Macro を作ってみた

https://qiita.com/nak435/items/e044daa296532912a424#%E3%82%BF%E3%83%97%E3%83%AB%E3%82%92equatablecomparable%E3%82%84hashable%E3%81%AB%E6%BA%96%E6%8B%A0%E3%81%95%E3%81%9B%E3%82%8B

以前書いた ↑ 記事の中で『タプルをEquatable、ComparableやHashableに準拠させる』方法として、次の`struct`を定義する方法を提案しました。

```swift
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`

とします。

```swift:MyMacro.swift
@freestanding(declaration, names: named(HashableTuple))
public macro GenarateHashableTuple<T>(_ value: T) = #externalMacro(module: "MyMacroMacros", type: "HashableTupleMacro")
```

# マクロの実装

```swift:MyMacroMacro.swift
@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`に準拠している必要がある

```swift:使い方
#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」を選択すると、生成されたコードが表示される。

```swift: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 を使ってインポートする方法は別記事に譲ります。




**以上**