Conform InlineArray to Hashable

InlineArray: Hashable

Summary of changes

InlineArray should conform to Equatable and Hashable when Element does.

Motivation

Not having these conformances is pretty annoying.

Proposed solution

Add the missing conformances.

Detailed design

That's it. That's the whole design.

Ok, fine:

  • These conformances implement elementwise equality, not identity.
  • The element hashes are mixed in index order.
  • Equality is permitted but not required to early-out (i.e. the implementation does not guarantee that InlineArray.== is constant time, nor does it guarantee not to access any elements after the first mismatch).

Source compatibility

If you wrote your own extension to provide these conformances, you should be able to delete it with minimal fuss, unless you implemented wildly different semantics for some reason. That was a bad idea.

We are adding an Equatable conformance to another type. This slightly enlarges the set of candidates for expressions involving == or !=, and might result in needing to break up or disambiguate expressions that are right at the limit of typechecker performance today.

ABI compatibility

N/A

Implications on adoption

The actual protocol conformance implementations will back-deploy to the introduction of InlineArray, allowing users to declare their own retroactive conformance if needed.

Future directions

None worth mentioning.

Alternatives considered

Implement identity rather than elementwise equality. This is arguably also a valid choice, but it's neither interesting nor particularly useful.

25 Likes

To answer the inevitable Chesterton's Fence question: it didn't make sense to include these when InlineArray was first added to the stdlib, because we didn't have SE-0499 yet. Now we do, and we should add these.

It's reasonable to suggest that we should add more of the conformances unlocked by 499, but Equatable and Hashable are by far the most painful missing ones and I'd like to get them squared away.

4 Likes

Is there a reason an implementation would not want to short-circuit? Or is this meant mainly to speak to implementations for BitwiseCopyable elements that might operate on the raw memory (e.g., an InlineArray<N, UInt8> might end up looking at more bytes than strictly the ones it needs to because of how they get loaded into memory)?

1 Like

Is it wise to document this?

1 Like

Sure, someone trying to write elliptic curve primitives on a type backed by InlineArray probably wants a constant time comparison. We are not making that guarantee for this API (partially because it's hard to really deliver in almost any language other than assembly targeting a specific uArch, partially because other use cases do not want it).

In the other direction, yeah, if you have an InlineArray<16, UInt8> on arm64 or x86, we want to either load 64-bit words, xor pairs, and check for zero, or just use a SIMD comparison. Either one of those unconditionally loads all sixteen bytes.

9 Likes

We should at least guarantee linear time for both operations. While this might seem obvious, the trivial implementation emits a linear copy per iteration resulting in quadratic execution time.

9 Likes

Happily the trivialest implementation in the stdlib is: lhs.span._elementsEqual(to: rhs.span), which, while the codegen still isn't quite perfect, does achieve O(n).

Yes, we can add that guarantee.

7 Likes

Maybe not a very compelling reason not to short-circuit… but if InlineArray can deliver a Span in guaranteed constant time and that Span can deliver a constant-time check for identity then IIUC there can at least be a legit workaround for that with existing public APIs.

Is the question whether or not we should short-circuit in the negative direction? If two InlineArray values have a different count value we return false right away?

Oops. Ditto for A + B, etc... Worth to know!

I remember experimenting with inline arrays before and being surprised why there were not (much or at all) faster compared to normal arrays. This behaviour explains why.

1 Like

The beauty of the count being part of the type is that comparing two inline arrays will always have the same count, so this fast path is done at compile time :slightly_smiling_face:

10 Likes

Are non-copyable element types supported? You should still spell out the actual extension declaration and where clause here.

If you mean that the witnesses are @_alwaysEmitIntoClient, that's insufficient. Witness tables don't backward deploy today, so use of these conformances in generic context would be limited to newer deployment targets.

The pitch says that retroactive conformances declared by users can depend on the witnesses from the standard library because those witnesses are back deployed. The witness tables for the canonical conformance shouldn't play a role in these retroactive conformances.

1 Like

Yes, that's the reason why this had to wait for 499.

1 Like

SE-0283 previously failed because Swift has no mechanism to conform a non-nominal type to a protocol.

Has that hurdle been solved? Could we now re-raise SE-0283? Or is this proposal about to run straight into the same wall?

[Edit] I guess it is actually a nominal type, so there isn't actually the same problem at all.

1 Like