I don't think so... as you say, that's what ===
is for. I haven't tried in a while though, maybe that's changed since I last needed a class to conform to Equatable
.
Obj-C classes imported into Swift receive a ==
implementation automatically using pointer equality. Swift classes do not receive such a default.
Ah, I did not know that. Is it just a free ==
function, or do they automatically conform to Equatable
, too?
Equatable
(implemented using isEqual
) and Hashable
(implemented using hash
).
Hmm... I'm inclined to say that this is unfortunate, but I obviously wasn't paying attention to those discussions or this wouldn't be the first I'd heard of it.
My understanding is that Swift ==
for Obj-C class instances is actually implemented using the Obj-C -[isEqual:]
method, whose default implementation in NSObject is pointer equality.
For classes that override -[isEqual:]
, including NSString
and collection classes like NSArray
, Swift's ==
will therefore use the override, not pointer equality.
This is likely documented somewhere, but I don't know where.
Probably because objects having the same identifier isn't the only way for them to be considered "equal".
Well yes, that's the point I was trying to make with this argument: this default implementation for Equatable
is just as correct or incorrect as the default implementation for Identifiable
:
In its very first sentence, the proposal states the goal of the Identifiable
protocol is to "correlate snapshots of the state of an entity in order to identify changes". In other words: to check if different instances are snapshots of the same entity. Similarly, the goal of the Equatable
protocol is to compare the state of two different instances to see if they hold the same state and can be used interchangeably.
For both protocols, the case where the two instances are identical is trivial. Both default implementations handle only this case. Most users will simply accept this default, assume it's correct and we can't blame them for doing so.
If the Standard Library offers a default implementation, it should be correct in all cases where it's offered. Do we really think users are going to look for and read the protocol's documentation if they can just add conformance to their class and not get any warnings or errors? As someone who teaches programming to young people, I can assure you they will just move on and assume everything is fine.
It is up to both the conformer and the receiver of the protocol to document the nature of the identity.
How does this rhyme with a default implementation that assumes object identity is the default? How does this encourage (reading of) documentation?
While providing a default implementation could lead to bugs from conformances where it isn't appropriate,
How does this rhyme with Swift's focus on safety? I'm under the impression that Swift is willing to sacrifice some convenience for safety. Isn't this why we don't implicitly convert between number types, why we require addtional work to support and unwrap optional values, why we have the override
keyword, why we require an explicit self
in closures, and so on? Why should we accept anything that "could lead to bugs" just to add some convenience for some use cases?
I feel like my understanding of these topics is still incomplete, but I don't seem to be the only one who still has serious concerns about this protocol. Here are some that remain unanswered:
If this protocol isn't exclusively about record/entity identity, what other forms of identity can it represent? Object identity was mentioned as an example, but does that even make sense? We already have object identity, why should we allow a protocol that can be adopted by structs and enums to represent object identity?
If this protocol is added to the Standard Library, shouldn't there also be an operator for it? Many people (myself included) conform classes to Equatable
by comparing only some identifying property. Now that we have an Identifiable
protocol, shouldn't we encourage users to implement that instead and stop conforming to Equatable
just to get an operator?
Similarly, shouldn't the Standard Library collections treat the different forms of identity on the same level? What if I want to check if a collection contains an instance with the same identity as some given instance? The ergonomics of the Standard Library encourage users to implement Equatable
, even when that isn't correct protocol for the job.
For example, here's some code I often write that relies on Equatable
but is actually about record identity because it only cares about the (record) identity of the instances, not their object identity (they are different objects) and not equality (their state isn't required to be identical):
if user == activity.host { ... }
if activity.participants.contains(user) { ... }
Well yes, that's the point I was trying to make with this argument: this default implementation for
Equatable
is just as correct or incorrect as the default implementation forIdentifiable
:
Can you give me an example where 2 value returning true for pointer equality are not equals ?
Can you give me an example where 2 value returning true for pointer equality are not equals ?
Yes: objects that define their equality on some mutable shared state which can mutate during the == comparison.
For example, a "FileContent", or a "CurrentDate", or, for the sake of the argument, a simple pointer.
For such objects, "identity implies equality" could not be guaranteed.
Can you give me an example where 2 value returning true for pointer equality are not equals ?
It's the inverse we should worry about: Yes, identical objects are equal, but objects can be equal (interchangeable) without being identical. So the problem is not that the default implementation returns true when it should return false, it's that it can return false where a correct implementation should return true.
For example:
class Product {
let code: String
var description: String
var price: Double
init(code: String, description: String, price: Double) {
self.code = code
self.description = description
self.price = price
}
}
extension Product: Identifiable { }
extension Product: Equatable { } // Assuming my default implementation.
let p1 = Product(code: "CH1", description: "Chair", price: 1)
let p2 = Product(code: "CH1", description: "Chair", price: 1)
let p3 = Product(code: "CH1", description: "Chair", price: 1.1)
In this (fairly common) example, both default implementations are wrong:
- All three objects represent snapshots of the same persistent entity. Again, the proposal states in its very first line that the purpose of
Identifiable
is to "correlate snapshots of the state of an entity in order to identify changes". Therefore, these three objects should be considered identical (according toIdentifiable
), yet they all have differentid
s. Without a default implementation, users would be forced to make a decision themselves, and would most likely implementid
by returningcode
, which is the correct implementation here. - Similarly, my default implementation of
Equatable
is incorrect because it doesn't considerp1
andp2
equal. Remember that equality is about value, not identity.p1
andp2
represent the same state of the entity, can be used interchangeable and therefore should be considered equal values.
Can you give me an example where 2 value returning true for pointer equality are not equals ?
Maybe I missed some posts, but to me it looks like the factor of time didn't receive enough attention:
I think it's extremely common that IDs can be persisted - and when you use ObjectIdentifier
, this will break terribly when objects are recreated (when, for example, an app is restarted).
Anyone who ever worked with a plain old relational database knows that one major reason to use IDs is expressing relationships, and with the default implementation, you have to be really careful not to forget you custom id
in this scenario...
If the Standard Library offers a default implementation, it should be correct in all cases where it's offered.
I don't agree with this particular default conformance to Identifiable
, but this statement is not correct, or at least doesn't describe the current state of things in Swift. It would be more accurate to say that it should be correct in most cases, otherwise almost no default implementation would be offered, e.g. the current synthesis of Equatable
and Hashable
conformance would be “wrong”.
I personally think the default implementation for Identifiable
is bad, but not because it's often the wrong implementation. I think the default implementation Identifiable
for objects is bad because knowing whether it is wrong or not requires reasoning about the lifetime of objects. Most people have little knowledge of how Swift manage the lifetime of objects and will assume "new object == different identity", even though it's not the case because creating new objects will reuse addresses (and thus identities) of deallocated ones.
And to make things worse, address reuse is more or less deterministic, so the failures of this default implementation will be non-deterministic.
Equality of non final class is a complex subject. The definition of correct
implementation is highly subjective even for user defined implementation.
class Product {
let code: String
var description: String
var price: Double
init(code: String, description: String, price: Double) {
self.code = code
self.description = description
self.price = price
}
}
class SpecificProduct :Product {
internal var note: String?
}
class ObservableProduct: Product {
var objectWillChange: Producer
override var price: Double {
willSet { objectWillChange.send() }
}
}
extension Product: Identifiable { }
extension Product: Equatable { } // Assuming my default implementation.
let p1 = Product(code: "CH1", description: "Chair", price: 1)
let p2 = SpecificProduct(code: "CH1", description: "Chair", price: 1)
let p3 = ObservableProduct(code: "CH1", description: "Chair", price: 1)
Defining the correct
implementation for Equatable for Product is left as an exercise to the reader.
That said, it's true that the default Identifiable
implementation will probably does not fit many cases. Most of type will probably have to provide an ID explicitly, so it does not bring real value. One major drawback I see is that when comforting to a protocol, I like to have the compiler tell me what I have to implements, and in that case, I will have to read the doc as the compiler will not complains about the missing property.
I still fail to see that ObjectIdentifier
is a common case for class-bound Identifiable
.
Perhaps that's what most argument against default implementations is about.
Assuming this is the case. A identifiable
consumer will incorrectly detect a 'remove' / 'insert' as an 'update' operation. Is this that bad ?
For instance, a List displaying your objects, will replace a row instead of removing one and inserting a new one.
I can't think about a case where it would cause real trouble, especially if you use the default implementation, as it means your objects don't have a strong meaning for ID.
I guess at worse in SwiftUI it'll result in the wrong animation, which is wrong but only a temporary glitch. Other consumers could persist this however. If you are keeping counts of events in relation to an ID, those stats will be wrong. If you're keeping any association relative to an ID, associations to things that have disappeared could spontaneously get reassociated with other things.
It's not the end of the world and I can live with this, but it's sad that the default implementation is providing us with such an unreliable ID. On the positive side, people will learn a thing or two about address reuse the day they notice things don't always work correctly, and then they'll provide an ID of their own. Let's just hope they notice at an early enough stage.
I think the default implementation
Identifiable
for objects is bad because knowing whether it is wrong or not requires reasoning about the lifetime of objects.
It's worse than that! You need to first know that it can be wrong! Where you can get that information? By reading the documentation maybe? ObjectIdentifier
documentation is actively misleading by telling you that it's unique and saying nothing about the lifetime!
A
identifiable
consumer will incorrectly detect a 'remove' / 'insert' as an 'update' operation. Is this that bad ?
It's bad if the only thing you're using Identifiable
for is differentiating between remove/insert and update.
I can't think about a case where it would cause real trouble
Let's say you want to have a list of people that gave you GDPR consent. You cannot directly save references to objects, because there isn't a Set
that holds the contents weakly in swift stdlib, so you make it a Set
of person.id
. Alice granted you the consent, but then deleted her account. Then comes Bob and makes a new account. He didn't grant you the consent, but the system thinks he did. Oops, now you're breaking the law.
I cannot think of the opposite example - where you use the default implementation, persist the ids but there are no bugs. Maybe the key insight is that we aren't supposed to persist the ids? But in that case why is it a property, and not an operator?
I am locking this thread, as it seems to have become a fuller re-litigation/appeal discussion.
To be clear, this is a fine conversation to have, but it doesn't belong on the acceptance announcement thread. Many people follow the announcements thread to get updates on evolution proposals, but don't want to follow a full evolution discussion, especially not on an already-accepted proposal. We should try and keep the subsequent posts on them focused.
I'm going to create a new thread for this over on the discussion topic.