[Pitch] Default implementation of Hashable for AnyObject

No. This should be implemented according to the app logic. This is why Hashable exists in the first place. The app author should implement calculating the hash. This is how it works now, and how it should work.

The topic is so simple that ist does not require code samples. OK, I explain the third time: If you use Hashable, you should implement calculating the hash. This is how it works now, and how it should work. The OP suggests to skip the implementation of hash calculation by providing a default. This is convenient, but logically wrong. The proposed default leads to hashing identity instead of data, replacing equality with identity, which are different things.

If Joe and Jane have exactly $100 in their pockets, and the app logic relies on the amount in a pocket, they would be == to each other. But of course they have different identities. Please don't mix the two things.

Nothing stops one from implementing their own Hashable conformance instead of the default. It's just the default is a safer, well, default.

It's not so simple, as you can tell from the crash I demonstrated above. It would be nice for you to describe where that code goes wrong (is it in Set, MyObject, something else?), and a code sample demonstrating the problems you see would be helpful.

They aren't completely different. They have overlap, specifically for classes. Equality of classes should almost always be object identity.

3 Likes

Of course! Currently there is no default, and this is how things should be. So that you don't forget to implement it.

I already said where it goes wrong: Your code implies that two objects cannot have the same hash, and if they do, your code crashes. Actually having the same hash is totally legal.

Logically they serve totally different purposes.

This goes back to my message to you at the start of the thread: in Swift, two distinct objects generally should not have the same hash. And if objects hash on data, that data must be immutable (or else you get the crashes Brandon showed above), and should not have any long-living behavior (or else that behavior might get silently discarded when stored).

I think you are bringing baggage/assumptions with how to hash data types in object oriented languages like Ruby, Python, Java, and even Objective-C to an extent, where there are concepts like "plain old Java objects" (POJOs), which are immutable objects without behavior that should hash on their data. In Swift, however, we have value types, like structs/enums, which are our version of POJOs, except enforced at compile time and with local reasoning when it comes to mutation. And in Swift these value types trivially synthesize Hashable for you automatically.

This means that in Swift you almost never define a class as you would a POJO. You will almost always reach for a struct instead.

The one common exception is SwiftData models, which are classes. But guess what? SwiftData models come with an implementation of Hashable, and its object identity, not the data inside. That means even SwiftData's default implementation is the same as this thread's pitch.

I think if the language has been exercised long enough for frameworks like SwiftData to realize that the default should be object identity, there's no reason not to consider that to be a reasonable default for every reference type.

When we do reach for a class over a struct in Swift, it's generally because we need something that has behavior over time, like an @Observable model, and when we need that model to be hashable, so that it can be stored in a set or if you want to use a model in a SwiftUI navigation stack, the only reasonable thing to hash on is its object identity.

6 Likes

Maybe you're right. I have to admit that I don't understand modern Swift quite well.

(The only real reason I can think of to not ship a default implementation on AnyObject is that folks unfamiliar with Swift may make the assumption that the : Hashable they tacked onto their class is hashing things based on memberwise properties. This could be partially solved with learning and documentation, or I'd advocate for a HashableObject protocol in the standard library if we really want it to be spelled out and avoid confusion.)

This seems reasonable to me. AnyObject.== comparing identity is the only possible generic implementation.

I'm not super familiar with this area of SwiftUI, but if it's true that this is common then I think the right solution to this is for SwiftUI to add a NavigationPath.append(_ value: some AnyObject) method.

but pretty unheard of in Swift because we have something better: value types.

It's not so long ago that structs had a major negative impact on binary size. I haven't revisited this recently but as of Swift 2.0 the savings from using classes were massive. My employer's codebase still uses immutable classes instead of structs.

EDIT: I misunderstood this pitch to be suggesting that Hashable automatically be added to all classes, not that it simply provide a default impl to those that opted in. I deleted a bunch of text from this reply that isn't relevant to what's actually being proposed.

1 Like

But why then make a tambourine dance with hash/identity/whatever if you can use the === operator?

This is typically solved in the language by implementing a copy-on-write struct that wraps an underlying class. You should generally avoid using a class directly. Johannes Weiss of the NIO team gave a great talk about how they employ the technique: https://www.youtube.com/watch?v=iLDldae64xE

This would be nice, but is a band-aid for a more widespread problem. You'd also need an overload for navigationDestination(item:), and navigationDestination(for:), and if you ever want to hold onto a model in a Set, would we want an overload of insert for Set<AnyHashable>? And contains? And every other Set API?

1 Like

There are popular APIs that require Hashable and commonly need to take model objects, and so you end up writing the same boilerplate for every model class when the standard library could provide that implementation for you. This pitch isn't about invoking === vs. == directly, it's about ensuring that model objects can participate in these Hashable APIs without having to implement boilerplate every time.

2 Likes

I think the one who use these APIs should understand how the Hashable is used there, or end up in incorrect logic, especially relying on the proposed default.

But the proposed default is the only valid one for those APIs. In SwiftUI, an element in a navigation stack is tied to the lifetime of a presentation, if you hashed on anything other than identity, the screen would dismiss/re-present whenever the hashed value changed (and this is ignoring that if that hash changes the NavigationPath itself is now in a bad state that could crash, the Set problem Brandon illustrated earlier).

For example, if you have a screen with a counter model:

@Observable
class CounterModel {
  var count = 0
}

If you hashed on count and bound $model.count to a stepper, the CounterView would be dismissed and re-presented (or crash) whenever you increment or decrement count.

4 Likes

I don't dispute that, but I cannot understand that either. Not only I don't understand modern Swift quite well, I don't use and don't like SwiftUI at all. Call me a luddite if you wish. Not to mention Swift Concurrency.

SwiftUI is a special case. It's not Swift, it's a domain specific language, which I admittedly don't know and don't understand.

SwiftUI is arguably the most popular framework the language has. I don't think it can be argued that "it's not Swift."

But even aside from SwiftUI the problem exists where identity is a common thing to hash on. Off the top of my head:

  • KeyPath
  • Task
  • ObservationTracking.Token
  • Cancellable

It's common to want to store these in a Set, and so they must be hashable.

4 Likes

Why an identity needs to be hashed? Why it cannot be compared directly?

See:

2 Likes

Probably I mixed Hashable and Comparable... which both inherit from Equtable... But the OP's proposal explicitly implemented the == operator...

I'm -1 on this idea for exactly this reason, and not just for beginners/folks unfamiliar with Swift; it'd be easy for even experienced devs to forget to tack on : Hashable and rely on the implicit conformance. It feels like an equally-surprising gotcha that the default behavior for all objects would be to compare pointer identity only — you very easily end up with Sets and Dictionarys with values and keys that appear identical (so are easily assumed to not be unique), but implicitly only check for object identity.

It might feel like a burden to have to implement your own conformance today, but in my mind, it's the reasonably-safe and -explicit way to express that these are the semantics you actually intend.

This seems like a reasonable middle ground to me (modulo bikeshedding the name). You can explicitly opt in to these semantics if you'd like, but you can't accidentally fall into the trap.

8 Likes

One interesting thing to watch out for is that if an object with an identity-based automatic Hashable+Equatable conformance wants to add a non-identity-based Equatable implementation, it would also need to override hashing at the same time or risk severe performance problems. One time I accidentally made clicking a button in iMovie take 8 hours at 100% cpu by incorporating different state in -isEqual: and -hash on an ObjC type.

12 Likes