Exposing Equatable Aspects Best Practice

I’ve been reviewing the documentation for Equatable and the last sentence in this paragraph is not clear to me. It is suggesting some kind of best practice but I’m struggling to visualize what this means and the motivation.

Equality implies substitutability—any two instances that compare equally can be used interchangeably in any code that depends on their values. To maintain substitutability, the == operator should take into account all visible aspects of an Equatable type. Exposing nonvalue aspects of Equatable types other than class identity is discouraged, and any that are exposed should be explicitly pointed out in documentation.

I understand the first part: the “==“ method should check all visible aspects of a class/structure. That ensures your instances can be substituted for one another.

But I’m not sure what the last sentence is suggesting regarding “exposing nonvalue aspects” and why that is discouraged. This seems like an important warning for Equatable usage so I’d like to understand it.

I believe this is saying that types/structs that are Equatable should keep reference properties private. Why is this a best practice? One could implement “==“ to check the reference types are indeed equal, so what is the motivation to this suggestion and adverse affect if not followed?

1 Like

I take this to mean, for example, that if you have a class Product
containing zero or more factors, and a cache of the factors' product,

class Product {
	var factor: [Int]
	var cache: Int?
	var value: Int {
		if let p = cache {
			return p
		}
		let p = factor.reduce(1, *)
		cache = p
		return p
	}
}

then two instances of Product should compare equal if their value
members are identical, or if the content of their factor members are
identical, depending whether you intend numerical or "lexical" equality.
But the implementation detail, cache, because it does not affect the
value, should not effect Product equality.

Likewise, if you have a Sum,

class Sum {
	var product: [Product]
	var cache: Int?
	var value: Int {
		if let p = cache {
			return p
		}
		let p = product.sum(0, { $0.value + $1.value})
		cache = p
		return p
	}
}

then I think it's ok for the content in the Product references to
figure in equality, but the cache still should not.

Dave

4 Likes

This part of the documentation is basically rephrasing the preceding part; the wording is a bit confusing because it's using lots of different words for the same or similar concepts:

The advice is that you should:
"take into account"
(= evaluate in your implementation of the == operator for "interchangeab[ility] in any code" = evaluate in your implementation of the == operator for "substitutability")
all "visible"
(= "exposed" = observable by your users, which may correspond to public if you're a library vendor but doesn't correspond if you're writing a type for internal use)
"aspects"
(a deliberately vague term because it includes not just members (i.e., properties and methods), but also observable behaviors when the instance is passed as an argument to non-member functions that operate on it)
of an instance to be part of that instance's "value".

So, put another way, "nonvalue aspects" of an instance (= aspects which you don't consider to be part of its value = aspects which you do not "take into account" in your implementation of the == operator) shouldn't be exposed (i.e., there should not be visible differences in behavior when one instance is substituted for another that compares equal to it).

For classes, it is expected that two instances which point to distinct objects in memory can nonetheless compare equal to each other; that is to say, class identity (a === b) is not required for value equivalence (a == b), or put another way, we say that we don't consider a class instance's identity to be "part of its value." This is an explicit exception to the discouragement from exposing "nonvalue aspects" of Equatable types.

All other exceptions should be documented amply so that your users are not caught unawares when they find observable differences in behavior with two values that are supposed to be substitutable for each other.

4 Likes

@gnuoyd Thanks for the very helpful examples. That makes perfect sense and I can derive that from their statement after reading your description.

@xwu Your breakdown of their statement's components is great. I think the part that was most confusing was their usage of the word "exposing" but your explanation seems like a reasonable and correct interpretation.