[Accepted] SE-0206: Hashable Enhancements

The core team has accepted this proposal with the following revisions:

  • As @jrose noted, init(seed:)'s behavior may be misleading to users who expect it to enable deterministic hashing, and has no real benefit over a collection implementation seeding the hasher directly with combine calls. To avoid misleading users, and to simplify the interface, we strongly suggest not including init(seed:) in the public Hasher API, instead suggesting that hashed collection implementers seed their hashers manually.

  • As a further simplification, the core team observes that the many combine(bits:) overloads on Hasher that take fixed-sized integers don’t need to be public API. Instead, they can be @usableFromInline entry points invoked by @inlinable implementations of hash(into:) on the integer types themselves:

    extension Hasher {
      @usableFromInline internal mutating func _combine(bits: UInt8)
      /* etc. */
    }
    
    extension UInt8: Hashable {
      @inlinable public func hash(into hasher: inout Hasher) {
        hasher._combine(bits: self)
      }
    }
    

    This greatly reduces the public API surface area of Hasher, in particular eliminating the large overload set for combine(bits:), while preserving the performance characteristics of the proposed design, since using the generic combine<H: Hashable> method with a fixed-sized integer type would naturally inline down to a call to the corresponding @usableFromInline entry point. This would reduce the public Hasher API to only init(), combine<H: Hashable>(_: H), the combine(bits:) method that takes a raw buffer of hash input, and finalize() as public API.

  • The core team recommends that Hasher.finalize() be made a __consuming nonmutating method instead of a mutating method that invalidates the Hasher value. This saves some overhead from Hasher having to check its validation state, and looking forward to a future version of Swift with an ownership model and support for move-only values, would allow correct use of finalize() to be statically enforced on move-only Hasher instances.

With the exception of the last point, these were the same revisions suggested by the core team in our previous review of the proposal. In further study, @lorentey discovered that there is significant performance value to providing a one-shot hashing interface for standard library types, particularly pure-ASCII Strings, though the benefit for user-defined types will be less due to resilience. Therefore, while the implementation may use one-shot hashing as an implementation detail, the core team does not see the need to design a public API for this functionality at this time.

Thanks again to everyone for participating in the review!

19 Likes