0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-07-16

以前書いた ↑ 記事の中で『タプルを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

とします。

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

マクロの実装

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に準拠している必要がある

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

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 Elementhash関数に反映されています。

extensionをマクロで生成するとエラーになるため、extensionを使っていません。
エラー:”swift Macro expansion cannot introduce extension”

このタプル(struct)を Set型の要素や、Dictonary型keyとして使うことができます。sort/sortedも比較関数不要でソートできます。
大変便利ですね。

ただし、独自マクロのため、#GenarateHashableTupleマクロを 競プロ で使うことはできません。ローカルに生成/展開したコードをコピペして使ってください。


ここで作成したマクロを実際にパッケージ化して、他のSwiftプロジェクトで SwiftPM を使ってインポートする方法は別記事に譲ります。

以上

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
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?