[Pitch] Add RawSpan primitive to Hasher

Hi everyone, I have a short pitch to add a RawSpan primitive to Hasher to mirror the existing unsafe buffer pointer API. The swift-evolution PR is up at Add RawSpan primitive to Hasher by jmschonfeld · Pull Request #3490 · swiftlang/swift-evolution · GitHub and the contents of the proposal are below.


Add RawSpan primitive to Hasher

  • Proposal: SE-NNNN
  • Author: Jeremy Schonfeld
  • Review Manager: TBD
  • Status: Awaiting implementation
  • Implementation: Not yet implemented

Summary of changes

Adds a new primitive API to Hasher for mixing the bytes of a RawSpan into the hasher's state.

Motivation

Today, Hasher offers two main APIs for mixing data into its state:

  1. A generic API that accepts any Hashable value, deferring to that value's hash(into:) implementation.
  2. A primitive API that accepts an UnsafeRawBufferPointer and mixes in the contents of the buffer directly.

Swift now has the RawSpan type, a safe alternative to UnsafeRawBufferPointer. Developers should be able to use Hasher's primitive API without depending on unsafe APIs.

Proposed solution

Add a new primitive API to Hasher that accepts a RawSpan as its argument. The new API can be used as follows:

var values: InlineArray<_, UInt8> = /* ... */

var hasher = Hasher()
hasher.combine(bytes: values.bytes)

Detailed design

extension Hasher {
    @export(implementation)
    public mutating func combine(bytes: RawSpan)
}

Source compatibility

This change should have no impact on source compatibility. The new API overloads an existing one, but UnsafeRawBufferPointer and RawSpan are not ambiguous with each other, and any client-provided implementation of this API will shadow the version provided by the standard library.

ABI compatibility

No impact on ABI compatibility.

Implications on adoption

This API can be freely adopted and unadopted in source code, assuming a deployment target new enough to use the RawSpan type itself.

Future directions

RawSpan Hashable conformance

We may want to conform RawSpan itself to Hashable in the future. While that is worth investigating, this proposal is focused solely on bringing the existing Hasher primitives to parity with newer standard library types. If RawSpan were to gain a Hashable conformance, it would also become possible to pass a RawSpan to the existing combine<H: Hashable>(_:) API. That would not obsolete the API proposed here: even if RawSpan is a valid argument to the generic combine, it remains important to provide a primitive API for hashing bytes that does not require the use of unsafe APIs.

12 Likes

I think this is straightforward and useful. Do you think there's any room in the same proposal for a generic public mutating func combine<Value: ConvertibleToBytes>(_ value: Value)? Guillaume wants to add RawSpan.init<Value: ConvertibleToBytes>(_:) already (though, if you wanted to get ahead of that landing, you could still implement this combine with the unsafe equivalent).

I can see where you're coming from there, however I don't think this proposal is the right place for that. There are many APIs across the standard library and our SDKs that use RawSpan as a parameter. It sounds like the main reason to add a combine(_: some ConvertibleToBytes) is because the manual combine(bytes: RawSpan(bytesOf: someValue)) is too verbose. To solve that problem, I think it'd be better to discuss how to make that less verbose without requiring every span-taking API to provide a ConvertibleToBytes alternative (whether that be through implicit conversion, syntactic sugar, etc.). A broader discussion would make our APIs feel more cohesive, otherwise you could do hasher.combine(bytes: 20) but you couldn't do an equivalent like myRawCollection.append(copying: 20) where you might otherwise use a RawSpan.

Instead of discussing that here, I'd like to keep this focused on just updating the APIs to use newer practices following the same semantics instead of expanding their capabilities - but happy to continue that discussion as a separate pitch.