【Swift】Protocol で宣言したオブジェクトの等価評価を AnyHashable でシンプルにする

## 動機づけ

次のようにプロトコルで宣言したオブジェクトの等価性を扱いたいとします。

```swift:FooProtocol.swift
public protocol FooProtocol {
    var foo: String { get }
    var bar: Int { get }
    var baz: Double { get }
}
```

```swift:FooProtocolTest.swift
class FooProtocolTest: XCTestCase {

    var hoge: FooProtocol!
    var fuga: FooProtocol!

    func test_example() {
        hoge = ... // FooProtocol プロトコルに準拠したオブジェクト
        fuga = ... // FooProtocol プロトコルに準拠したオブジェクト

        XCTAssertEqual(hoge.foo, fuga.foo)
        XCTAssertEqual(hoge.bar, fuga.bar)
        XCTAssertEqual(hoge.baz, fuga.baz)
    }
}
```

このように、プロパティ各々を比較するのではなく、 `XCTAssertEqual(hoge, fuga)` のようにすることで、プロトコルで定義したプロパティをまとめて比較できれば便利そうです。

そのような単純化の方法について考えてみたいと思います。

## 【失敗】`Equatable` 準拠

Swift で等価性を扱う場合、通常は `Equatable` に準拠することになります。
しかし、この方法は簡単には上手くいきません。

たとえば、次のように `Equatable` のサブプロトコルとして定義します。

```swift:FooProtocol.swift
public protocol FooProtocol: Equatable {
    var foo: String { get }
    var bar: Int { get }
    var baz: Double { get }
}
```

このプロトコルに適合することで、`==` で評価することができるようになります。

```swift:Hoge.swift
public struct Hoge: FooProtocol {
    public var foo: String
    public var bar: Int
    public var baz: Double

    public var qux: [String]
}
```

```swift
let hoge1 = Hoge(foo: "ほげほげ", bar: 23, baz: 42, qux: ["ほげ"])
let hoge2 = Hoge(foo: "ほげほげ", bar: 23, baz: 42, qux: ["ほげ"])
print(hoge1 == hoge2) // true
```

しかし、これは `Hoge` としての等価性であって、`FooProtocol` で定義されたプロパティの等価性ではありません。
上記の場合は、`FooProtocol` で定義されていない変数 `qux` まで評価されます。

また、次のように `FooProtocol` に適合した別の型との比較はできません。

```swift:Fuga.swift
public struct Fuga: FooProtocol {
    public var foo: String
    public var bar: Int
    public var baz: Double

    public var quux: [Int : String]
}
```

```swift
let hoge = Hoge(foo: "ほげほげ", bar: 23, baz: 42, qux: ["ほげ"])
let fuga = Fuga(foo: "ほげほげ", bar: 23, baz: 42, quux: [1: "ほげ"])
print(hoge == fuga) // コンパイルエラー
```

`Hoge` と `Fuga` がそれぞれ `Equatable` に準拠しているだけなので、自分自身と同じ型でなければ `==` による比較ができないためです。

また、プロトコルを `Equatable` に準拠させたので、冒頭のテストコードはコンパイルエラーになります。

```swift:FooProtocolTest.swift
class FooProtocolTest: XCTestCase {
    // 次の理由でコンパイルエラー
    // Protocol 'FooProtocol' can only be used as a generic constraint 
    // because it has Self or associated type requirements
    var hoge: FooProtocol!
    var fuga: FooProtocol!
}
```

`Self` または associated type を使用すると上記のような型宣言ができなくなります。
`Equatable` が `Self` を使用したプロトコルなので、上記の通りコンパイルエラーになります。

```swift
public protocol Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool
}
```

## `Equatable` 準拠の問題点まとめ

失敗作の問題点をまとめると次の２点になります。

- `FooProtocol` で定義したプロパティの等価性ではなく、適合先の構造体の等価性になってしまった
- `FooProtocol` が `Self` を使用したプロトコルなので、型宣言ができなくなってしまった

上記から、`FooProtocol` 自体を `Equatable` のサブプロトコルにしてしまうのは、問題の解決にならないことがわかりました。

## 【成功】`AnyHashable` を利用する

冒頭の `XCTAssertEqual(hoge, fuga)` のように、`FooProtocol` 自体を `Equatable` にすることは諦めて、
次のような作戦をとることにしました。

```swift
XCTAssertEqual(hoge.fooHashable, fuga.fooHashable)
```

この `fooHashable` の部分が「プロトコルで定義されているプロパティを内部的に保持している何か」で、`Equatable` に準拠していているようにします。

具体的には、次のような実装になります。

```swift:FooProtocol.swift
public protocol FooProtocol {
    var foo: String { get }
    var bar: Int { get }
    var baz: Double { get }
}

public extension FooProtocol {
    var fooHashable: AnyHashable {
        return Anonymous(foo: foo, bar: bar, baz: baz)
    }
}

private struct Anonymous: FooProtocol, Hashable {
    let foo: String
    let bar: Int
    let baz: Double
}
```

コードを言葉で表現すると次のようになります。

- `Hashable` に適合したプライベートな型（Anonymous）を作る
- プロトコルのデフォルト実装で `AnyHashable` にラップして上記のオブジェクトを返す

こうすることで、`Anonymous` で定義されたプロパティの等価評価をすることになり、実質的には `FooProtocol` で定義しているプロパティをまとめて評価することができるようになります。

次のように異なる型で宣言していても、`FooProtocol` の意味での等価評価ができます。

```swift
let hoge: Hoge = Hoge(foo: "ほげほげ", bar: 23, baz: 42, qux: ["ほげ"])
let fuga: Fuga = Fuga(foo: "ほげほげ", bar: 23, baz: 42, quux: [1: "ほげ"])
print(hoge.fooHashable == fuga.fooHashable) // true
```

そして、冒頭のようなプロトコルで宣言したオブジェクトの等価評価も、プロパティの各々を比較する必要がなくなります。

```swift:FooProtocolTest.swift
class FooProtocolTest: XCTestCase {

    var hoge: FooProtocol!
    var fuga: FooProtocol!

    // 等価比較：FooProtocol の意味で一致していること
    func test_example_1() {
        hoge = Hoge(foo: "ほげほげ", bar: 23, baz: 42, qux: ["ほげ"])
        fuga = Fuga(foo: "ほげほげ", bar: 23, baz: 42, quux: [1: "ほげ"])
        XCTAssertEqual(hoge.fooHashable, fuga.fooHashable)
    }

    // 等価比較：FooProtocol の意味で一致していないこと
    func test_example_2() {
        // 「ふが」と「ほげほげ」の不一致
        hoge = Hoge(foo: "ふが", bar: 23, baz: 42, qux: ["ほげ"])
        fuga = Fuga(foo: "ほげほげ", bar: 23, baz: 42, quux: [1: "ほげ"])
        XCTAssertNotEqual(hoge.fooHashable, fuga.fooHashable)
    }
}
```

ということで、コードを少し簡潔にすることができました。

また、この実装は、`Hoge` や `Fuga` 自体の等価性と無関係な実装ですので、必要に応じて適合先の構造体やクラスで `Equatable` に準拠することも可能です。

そうすると、次のように `FooProtocol` と `Hoge` の２通りの意味での等価評価が可能になります。

```swift:Hoge.swift
public struct Hoge: FooProtocol, Equatable { // Equatable に準拠
    public var foo: String
    public var bar: Int
    public var baz: Double

    public var qux: [String]
}
```

```swift
let hoge1: Hoge = Hoge(foo: "ほげ", bar: 23, baz: 42, qux: ["ほげ"])
let hoge2: Hoge = Hoge(foo: "ほげ", bar: 23, baz: 42, qux: ["ほげ", "ふが"])

// `FooProtocol` の意味での評価は qux が無視されるので true
print(hoge1.fooHashable == hoge2.fooHashable) 

// Hoge の意味での評価は qux が不一致で false
print(hoge1 == hoge2) 
```

## 結び

`AnyHashable` と言えば、`[AnyHashable: Any]` のような辞書のキーとしての利用をよく見かけますが、`Equatable` から `Self` を型消去した等価評価のためのオブジェクトとしても利用できることが分かりました。

プロパティの個数が多い場合や、テストケースを増やしたい場合などに役に立ちそうなテクニックですね。