🦔

swift-concurrency-extrasのAnyHashableSendableについて

2024/12/06に公開

TCA Advent Calendar 2024の記事です。

はじめに

TCAに使われているswift-concurrency-extrasのAnyHashableSendableについて解説します。バージョンはv1.3です。

もし何か間違いがあったらコメントいただければ助かります。

AnyHashableSendableについて

めちゃくちゃ雑にいうと、任意のHashableかつSendableな値をラップするための構造体です。

Swiftでは、構造体AnyHashable を使用して任意のHashable値を型消去できます。
しかしAnyHashableはSendableに準拠していません。かつ、HashableプロトコルもSendableに準拠していません。AnyHashableSendableはこの制限を回避しつつ、並行性に対応したSendableなラッパーを提供します。

テスト

testAnyHashable

AnyHashable型がキャストできるかのテストをしているっぽいです。

  func testAnyHashable() {
    XCTAssertEqual(AnyHashableSendable(1), 1 as AnyHashable)
  }

testExistential

HashableにもSendableにも準拠している値をOptionalのメソッドまpでAnyHashableSendableに変換し、その結果がAnyHashableSendableに変換できたかをテストしてます。

func testExistential() {
    // 任意の Hashable かつ Sendable な値を保持するオプショナル変数を作成
    let base: (any Hashable & Sendable)? = 1
    // `base` が非 nil なら `AnyHashableSendable` にラップする
    let wrapped = base.map(AnyHashableSendable.init)
    // ラップされた値が期待通り `AnyHashableSendable(1)` と等しいことを検証
    XCTAssertEqual(wrapped, AnyHashableSendable(1))
}

Optionalにしているのは、
SwiftにはOptionalのmapメソッドがあるからです。
これはnilでなければ実行されます。

https://developer.apple.com/documentation/swift/optional/map(_:)

testBasics

AnyHashableSendableの値が同じなら等価で、そうでないなら等価じゃないというテストと、ネストされたAnyHashableSendableが再度ラップされてAnyHashableSendable(AnyHashableSendable(1))も、元のAnyHashableSendable(1)と等価であることを検証します。

  func testBasics() {
    XCTAssertEqual(AnyHashableSendable(1), AnyHashableSendable(1))
    XCTAssertNotEqual(AnyHashableSendable(1), AnyHashableSendable(2))

   // 任意の Hashable かつ Sendable な値を
   // `AnyHashableSendable` に変換するヘルパー関数を定義
    func make(_ base: some Hashable & Sendable) -> AnyHashableSendable {
      AnyHashableSendable(base)
    }

    let flat = make(1)
    let nested = make(flat)

    XCTAssertEqual(flat, nested)
  }

testLiterals

AnyHashableSendableさまざまなリテラル(ブール値、整数、浮動小数点数、文字列)から正しく初期化され、期待通りに動作することを検証しています。

  func testLiterals() {
    // XCTAssertEqualは異なる型を比較できる。
    // AnyHashableSendableの`==`で右辺をAnyHashableSendableにできる。
    XCTAssertEqual(AnyHashableSendable(true), true)
    XCTAssertEqual(AnyHashableSendable(1), 1)
    XCTAssertEqual(AnyHashableSendable(4.2), 4.2)
    XCTAssertEqual(AnyHashableSendable("Blob"), "Blob")

    // リテラルを使用して `AnyHashableSendable` を直接初期化
    let bool: AnyHashableSendable = true
    // `base` プロパティが正しくラップされていることを検証
    XCTAssertEqual(bool.base as? Bool, true)
    // 型キャスト後の等価性を検証
    XCTAssertEqual(bool as AnyHashable as? Bool, true)
  }

おそらくですが、

XCTAssertEqual(AnyHashableSendable(1), 1)は左辺をネストされたAnyHashableSendable(AnyHashableSendable(1))とし、右辺をAnyHashableSendable(1)としているのではないかと思います。

おわりに

TCAではID型を使う際に利用されているようです。

https://github.com/search?q=repo%3Apointfreeco%2Fswift-composable-architecture+AnyHashableSendable&type=code

Discussion