From 9cbc1df05b7b5b8e6a9b0db93b5cbf4702e2980f Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 18 Sep 2024 17:58:08 -0700 Subject: [PATCH 1/6] Add `AnyHashableSendable` --- README.md | 6 +++++ .../AnyHashableSendable.swift | 23 +++++++++++++++++++ .../Documentation.docc/ConcurrencyExtras.md | 1 + 3 files changed, 30 insertions(+) create mode 100644 Sources/ConcurrencyExtras/AnyHashableSendable.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..3d4d1ec --- /dev/null +++ b/Sources/ConcurrencyExtras/AnyHashableSendable.swift @@ -0,0 +1,23 @@ +/// 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) { + self.base = base + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + func open(_ lhs: T) -> Bool { + lhs == rhs as? T + } + return open(lhs) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(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 From 2ba2b7b3c402de5b9437718822c95f4309a2cb19 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 18 Sep 2024 18:04:39 -0700 Subject: [PATCH 2/6] conformances --- .../ConcurrencyExtras/AnyHashableSendable.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Sources/ConcurrencyExtras/AnyHashableSendable.swift b/Sources/ConcurrencyExtras/AnyHashableSendable.swift index 3d4d1ec..9178b33 100644 --- a/Sources/ConcurrencyExtras/AnyHashableSendable.swift +++ b/Sources/ConcurrencyExtras/AnyHashableSendable.swift @@ -21,3 +21,20 @@ public struct AnyHashableSendable: Hashable, Sendable { 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) + } +} From d631e7ff87f124f0ed738f1599b454a332186a37 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 18 Sep 2024 18:45:00 -0700 Subject: [PATCH 3/6] Tests --- .../AnyHashableSendable.swift | 10 +++++++--- .../AnyHashableSendableTests.swift | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 Tests/ConcurrencyExtrasTests/AnyHashableSendableTests.swift diff --git a/Sources/ConcurrencyExtras/AnyHashableSendable.swift b/Sources/ConcurrencyExtras/AnyHashableSendable.swift index 9178b33..d77da46 100644 --- a/Sources/ConcurrencyExtras/AnyHashableSendable.swift +++ b/Sources/ConcurrencyExtras/AnyHashableSendable.swift @@ -7,14 +7,18 @@ public struct AnyHashableSendable: Hashable, Sendable { /// Creates a type-erased hashable, sendable value that wraps the given instance. public init(_ base: some Hashable & Sendable) { - self.base = base + if let base = base as? AnyHashableSendable { + self = base + } else { + self.base = base + } } public static func == (lhs: Self, rhs: Self) -> Bool { func open(_ lhs: T) -> Bool { - lhs == rhs as? T + lhs == rhs.base as? T } - return open(lhs) + return open(lhs.base) } public func hash(into hasher: inout Hasher) { diff --git a/Tests/ConcurrencyExtrasTests/AnyHashableSendableTests.swift b/Tests/ConcurrencyExtrasTests/AnyHashableSendableTests.swift new file mode 100644 index 0000000..913e9f2 --- /dev/null +++ b/Tests/ConcurrencyExtrasTests/AnyHashableSendableTests.swift @@ -0,0 +1,19 @@ +import ConcurrencyExtras +import XCTest + +@available(*, deprecated) +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) + } +} From bd5eaffb747f02baebe191085ed62f02a990836b Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 18 Sep 2024 20:12:22 -0700 Subject: [PATCH 4/6] Update AnyHashableSendable.swift --- Sources/ConcurrencyExtras/AnyHashableSendable.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ConcurrencyExtras/AnyHashableSendable.swift b/Sources/ConcurrencyExtras/AnyHashableSendable.swift index d77da46..fc077c8 100644 --- a/Sources/ConcurrencyExtras/AnyHashableSendable.swift +++ b/Sources/ConcurrencyExtras/AnyHashableSendable.swift @@ -31,6 +31,7 @@ extension AnyHashableSendable: CustomDebugStringConvertible { "AnyHashableSendable(" + String(reflecting: base) + ")" } } + extension AnyHashableSendable: CustomReflectable { public var customMirror: Mirror { Mirror(self, children: ["value": base]) From 4d98dfbdcbba7d34fe9f71d5be58fe942f24f847 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 18 Sep 2024 20:12:41 -0700 Subject: [PATCH 5/6] Update AnyHashableSendableTests.swift --- Tests/ConcurrencyExtrasTests/AnyHashableSendableTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/ConcurrencyExtrasTests/AnyHashableSendableTests.swift b/Tests/ConcurrencyExtrasTests/AnyHashableSendableTests.swift index 913e9f2..488d6b7 100644 --- a/Tests/ConcurrencyExtrasTests/AnyHashableSendableTests.swift +++ b/Tests/ConcurrencyExtrasTests/AnyHashableSendableTests.swift @@ -1,7 +1,6 @@ import ConcurrencyExtras import XCTest -@available(*, deprecated) final class AnyHashableSendableTests: XCTestCase { func testBasics() { XCTAssertEqual(AnyHashableSendable(1), AnyHashableSendable(1)) From 2695f10932275b2af3da24b5b9b365fddbfd543e Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 19 Sep 2024 09:39:17 -0700 Subject: [PATCH 6/6] Update AnyHashableSendable.swift --- Sources/ConcurrencyExtras/AnyHashableSendable.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/ConcurrencyExtras/AnyHashableSendable.swift b/Sources/ConcurrencyExtras/AnyHashableSendable.swift index fc077c8..e4a510b 100644 --- a/Sources/ConcurrencyExtras/AnyHashableSendable.swift +++ b/Sources/ConcurrencyExtras/AnyHashableSendable.swift @@ -15,10 +15,7 @@ public struct AnyHashableSendable: Hashable, Sendable { } public static func == (lhs: Self, rhs: Self) -> Bool { - func open(_ lhs: T) -> Bool { - lhs == rhs.base as? T - } - return open(lhs.base) + AnyHashable(lhs.base) == AnyHashable(rhs.base) } public func hash(into hasher: inout Hasher) {