From 6054df64b55186f08b6d0fd87152081b8ad8d613 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 19 Sep 2024 10:39:12 -0700 Subject: [PATCH] Add `AnyHashableSendable` (#36) * Add `AnyHashableSendable` * conformances * Tests * Update AnyHashableSendable.swift * Update AnyHashableSendableTests.swift * Update AnyHashableSendable.swift --- README.md | 6 +++ .../AnyHashableSendable.swift | 42 +++++++++++++++++++ .../Documentation.docc/ConcurrencyExtras.md | 1 + .../AnyHashableSendableTests.swift | 18 ++++++++ 4 files changed, 67 insertions(+) create mode 100644 Sources/ConcurrencyExtras/AnyHashableSendable.swift create mode 100644 Tests/ConcurrencyExtrasTests/AnyHashableSendableTests.swift diff --git a/README.md b/README.md index d5e3415..7406a9f 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ This library comes with a number of tools that make working with Swift concurren testable. * [`LockIsolated`](#lockisolated) + * [`AnyHashableSendable`](#anyhashablesendable) * [Streams](#streams) * [Tasks](#tasks) * [Serial execution](#serial-execution) @@ -44,6 +45,11 @@ testable. The `LockIsolated` type helps wrap other values in an isolated context. It wraps the value in a class with a lock, which allows you to read and write the value with a synchronous interface. +### `AnyHashableSendable` + +The `AnyHashableSendable` type is a type-erased wrapper like `AnyHashable` that preserves the +sendability of the underlying value. + ### Streams The library comes with numerous helper APIs spread across the two Swift stream types: diff --git a/Sources/ConcurrencyExtras/AnyHashableSendable.swift b/Sources/ConcurrencyExtras/AnyHashableSendable.swift new file mode 100644 index 0000000..e4a510b --- /dev/null +++ b/Sources/ConcurrencyExtras/AnyHashableSendable.swift @@ -0,0 +1,42 @@ +/// A type-erased hashable, sendable value. +/// +/// A sendable version of `AnyHashable` that is useful in working around the limitation that an +/// existential `any Hashable` does not conform to `Hashable`. +public struct AnyHashableSendable: Hashable, Sendable { + public let base: any Hashable & Sendable + + /// Creates a type-erased hashable, sendable value that wraps the given instance. + public init(_ base: some Hashable & Sendable) { + if let base = base as? AnyHashableSendable { + self = base + } else { + self.base = base + } + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + AnyHashable(lhs.base) == AnyHashable(rhs.base) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(base) + } +} + +extension AnyHashableSendable: CustomDebugStringConvertible { + public var debugDescription: String { + "AnyHashableSendable(" + String(reflecting: base) + ")" + } +} + +extension AnyHashableSendable: CustomReflectable { + public var customMirror: Mirror { + Mirror(self, children: ["value": base]) + } +} + +extension AnyHashableSendable: CustomStringConvertible { + public var description: String { + String(describing: base) + } +} diff --git a/Sources/ConcurrencyExtras/Documentation.docc/ConcurrencyExtras.md b/Sources/ConcurrencyExtras/Documentation.docc/ConcurrencyExtras.md index 1765ca9..e40491a 100644 --- a/Sources/ConcurrencyExtras/Documentation.docc/ConcurrencyExtras.md +++ b/Sources/ConcurrencyExtras/Documentation.docc/ConcurrencyExtras.md @@ -236,6 +236,7 @@ need to make weaker assertions due to non-determinism, but can still assert on s ### Data races - ``LockIsolated`` +- ``AnyHashableSendable`` ### Serial execution diff --git a/Tests/ConcurrencyExtrasTests/AnyHashableSendableTests.swift b/Tests/ConcurrencyExtrasTests/AnyHashableSendableTests.swift new file mode 100644 index 0000000..488d6b7 --- /dev/null +++ b/Tests/ConcurrencyExtrasTests/AnyHashableSendableTests.swift @@ -0,0 +1,18 @@ +import ConcurrencyExtras +import XCTest + +final class AnyHashableSendableTests: XCTestCase { + func testBasics() { + XCTAssertEqual(AnyHashableSendable(1), AnyHashableSendable(1)) + XCTAssertNotEqual(AnyHashableSendable(1), AnyHashableSendable(2)) + + func make(_ base: some Hashable & Sendable) -> AnyHashableSendable { + AnyHashableSendable(base) + } + + let flat = make(1) + let nested = make(flat) + + XCTAssertEqual(flat, nested) + } +}