Add AnyEquatable to standard library

The standard library already includes a AnyHashable that can be very useful. I propose adding a AnyEquatable type.

The implementation of this would be very similar to the AnyHashable type since the Hashable conformance already requires that type to conforms too Equatable.

Like AnyHashable I believe it would be good to add some connivance methods to some collection types where Element == AnyEquatable.

2 Likes

Do you mean any equatable ?

1 Like

What are the use cases you have in mind for such a type?

1 Like

It used to be useful, before casting broke a few versions ago. They'll have to fix that before it can be worthwhile again.

1 Like

Mostly for building any-type Equatable Collections. Since Equatable protocol has a Self reference you can't do Array<Equatable>().

A concrete example can be found in my KeyWindow package. So that it can make use of the SwiftUI's Preferences api it needs to ensure the Preference value is Equatable (otherwise we are unable to read this value higher up). This api needs to collect multiple preference values when reducing over child views. However it is not using a single concrete type so the collection needs to use a any-type however the collection also needs to be equatable hence why this package currently contains its own AnyEquatable implementation derived from the AnyHashable that can be found in the standard library.

The question is how you want AnyEquatable to actually implement the == operator.

Standard library currently has two very different erasing wrappers over types that conform to Equatable (or an inherited protocol): one being AnyHashable, as you mention, but there's also AnyIndex, which abstracts Comparable.

There is a good reason to have AnyHashable as is, because it is able to work "standalone": a hashable value can produce its hash regardless of its Self constraint, and here the fact that Hashable inherits from Equatable is a more semantical constraint than anything inherently required for hashing to work. If you would try to compare two AnyHashables with different underlying types, it will gracefully return false.

On the other hand, AnyIndex is an abstraction to be able to work with collections regardless of their index types, but indices can't really do anything as "standalone" values: they are tied to startIndex and endIndex of a collection, for example, and such still need to be able to compare as their underlying types. Because of this, comparing two unrelated AnyIndexes is a semantic disaster, so it will actually crash at runtime if you'd try to.

So the question is, how should AnyEquatable actually behave when trying to equate two underlying values? On the one hand, it seems reasonable that AnyEquatable(5) == AnyEquatable("5") should return false — that is, you just assume that diffrent types are incomparable — but should AnyEquatable(5 as Int) == AnyEquatable(5 as Int8) return false? Or should these crash?

More generally, I beleive there already have been some discussions on enabling the compiler to sythesize Any<Whatever> wrappers over protocols with Self and stuff, so it might be a much more generic question of whether protocols should be granted this ability and what their default runtime behaviour should be.

3 Likes

This is not exactly a semantical constraint. Hashing is useless without the possibility to check equality of values with the same hash.

And the fact that it gracefully returns false for different types is an unfortunate limitation, as it prevents Heterogeneous lookup.

1 Like

I only want it if it's generic, à la AnySequence and its derivatives. See my implementation above for what used to work.

Right now, that means that it would have to be the same as AnyHashable: two instances that can't both be cast to a known type must be "not equal". But that's only because Equatable is old and didn't get updated for Swift 2. Equatable.== should be a throwing function.

/// An `Equatable` instance is stored as a "`Cast`".
/// Only instances that can be cast to that type can be `==`'d with the `AnyEquatable`.
public struct AnyEquatable<Cast> {

Interesting idea, so Cast would need to conform too Equatable? That is different from AnySequence were they is no type constraint on the Element type. Requiring the element type to conform to Equatable implicitly means you can only put a concrete type there. You can't put a Protocol (due to the limitations of swifts type system)..

I would suggest keep AnyEquatable as a generic free type and like AnyHashable add a protocol that you can optionally conform to that is used in the constructor of AnyEquatable (where the E: HasCustomAnyEquatableRepresentation ) that exposes the needed down-cast values for comparison.

Have you attempted to use the _HasCustomAnyHashableRepresentation protocol for AnyHashable? The exact implementation used by AnyHashable for this to me seems a little strange since the AnyHashableBox protocol is not public so you can't create a AnyHashable using a custom box type all you can do is cast the wrapped value. In addition rather than using a where H: _HasCustomAnyHashableRepresentation not he constructor this is does using a cast operation in the general constructor (maybe doe to the game of AnyHashable?).


edit: see KeyWindowTests/AnyEquatableTests.swift as an example of how this could work.

As I bug-reported (linked above), this only became true recently. It used to work just fine. I had tests. Now I'm XCTSkipping them until things work properly again.

Terms of Service

Privacy Policy

Cookie Policy