Skip to content

Commit

Permalink
AnyHashableSendable ergonomics (#47)
Browse files Browse the repository at this point in the history
* `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
  • Loading branch information
stephencelis authored Nov 11, 2024
1 parent 524fd4b commit 6b0576e
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 0 deletions.
36 changes: 36 additions & 0 deletions Sources/ConcurrencyExtras/AnyHashableSendable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
21 changes: 21 additions & 0 deletions Tests/ConcurrencyExtrasTests/AnyHashableSendableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

0 comments on commit 6b0576e

Please sign in to comment.