From 6b0576e9b968cccde0b1435660185bbacebf8ce1 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 11 Nov 2024 10:05:02 -0800 Subject: [PATCH] `AnyHashableSendable` ergonomics (#47) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * `AnyHashableSendable` ergonomics While working with this type I noticed a couple deficiencies: - First, the initializer takes an opaque type rather than an existential, and while existentials are typically automatically opened, there are cases in which this initializer will fail, _e.g._: ```swift let x: (any Hashable & Sendable)? x.map(AnyHashableSendable.init) // 🛑 ``` We can fix this by updating the initializer to be an `any` already. - Second, comparing an `AnyHashableSendable` with another `AnyHashable` fails because of the underlying type, but there is an underscored public protocol in the standard library we can take advantage of that is called when a hashable type is cast to `AnyHashable`, and if we return the base value then things like this start to work: ```swift AnyHashableSendable(1) == 1 as AnyHashable // true ``` * Add basic literals * fix * fix --- .../AnyHashableSendable.swift | 36 +++++++++++++++++++ .../AnyHashableSendableTests.swift | 21 +++++++++++ 2 files changed, 57 insertions(+) diff --git a/Sources/ConcurrencyExtras/AnyHashableSendable.swift b/Sources/ConcurrencyExtras/AnyHashableSendable.swift index e4a510b..c7691fb 100644 --- a/Sources/ConcurrencyExtras/AnyHashableSendable.swift +++ b/Sources/ConcurrencyExtras/AnyHashableSendable.swift @@ -5,6 +5,12 @@ public struct AnyHashableSendable: Hashable, Sendable { public let base: any Hashable & Sendable + /// Creates a type-erased hashable, sendable value that wraps the given instance. + @_disfavoredOverload + public init(_ base: any Hashable & Sendable) { + self.init(base) + } + /// Creates a type-erased hashable, sendable value that wraps the given instance. public init(_ base: some Hashable & Sendable) { if let base = base as? AnyHashableSendable { @@ -40,3 +46,33 @@ extension AnyHashableSendable: CustomStringConvertible { String(describing: base) } } + +extension AnyHashableSendable: _HasCustomAnyHashableRepresentation { + public func _toCustomAnyHashable() -> AnyHashable? { + base as? AnyHashable + } +} + +extension AnyHashableSendable: ExpressibleByBooleanLiteral { + public init(booleanLiteral value: Bool) { + self.init(value) + } +} + +extension AnyHashableSendable: ExpressibleByFloatLiteral { + public init(floatLiteral value: Double) { + self.init(value) + } +} + +extension AnyHashableSendable: ExpressibleByIntegerLiteral { + public init(integerLiteral value: Int) { + self.init(value) + } +} + +extension AnyHashableSendable: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self.init(value) + } +} diff --git a/Tests/ConcurrencyExtrasTests/AnyHashableSendableTests.swift b/Tests/ConcurrencyExtrasTests/AnyHashableSendableTests.swift index 488d6b7..80eb747 100644 --- a/Tests/ConcurrencyExtrasTests/AnyHashableSendableTests.swift +++ b/Tests/ConcurrencyExtrasTests/AnyHashableSendableTests.swift @@ -15,4 +15,25 @@ final class AnyHashableSendableTests: XCTestCase { XCTAssertEqual(flat, nested) } + + func testExistential() { + let base: (any Hashable & Sendable)? = 1 + let wrapped = base.map(AnyHashableSendable.init) + XCTAssertEqual(wrapped, AnyHashableSendable(1)) + } + + func testAnyHashable() { + XCTAssertEqual(AnyHashableSendable(1), 1 as AnyHashable) + } + + func testLiterals() { + XCTAssertEqual(AnyHashableSendable(true), true) + XCTAssertEqual(AnyHashableSendable(1), 1) + XCTAssertEqual(AnyHashableSendable(4.2), 4.2) + XCTAssertEqual(AnyHashableSendable("Blob"), "Blob") + + let bool: AnyHashableSendable = true + XCTAssertEqual(bool.base as? Bool, true) + XCTAssertEqual(bool as AnyHashable as? Bool, true) + } }