Time complexity is undecidable because of Rice's theorem - Wikipedia, so you’d need to come up with an approximate solution of some sort: either you have a strict type system that rejects some valid programs that it cannot determine the time complexity of, or you have a conservative analysis that spots certain “obvious” patterns that are not O(1), but doesn’t catch all of them.
Example implementation of Array's contains that uses tri-state "bikeShedNameHere" method as an optimisation.
func contains(_ element: Element) -> Bool {
var notEqualCount = 0
for v in self {
if let equal = element.bikeShedNameHere(v) { // O(1)
if equal { // identical, thus equal
// quick path -> yes
return true
} else { // quick not equal
notEqualCount += 1
}
}
}
if notEqualCount == count {
// quick path -> no
return false
}
// slow path
for v in self {
if element == v {
return true
}
}
return false
}
Re the name: "distinguishable" or "identical", or similar. Looks they are not great if we want to return a tri-state value including "definitely not equal".
protocol BikeShedNameHere {
func bikeShedNameHere(_ other: Self) -> Bool? // or a tri-state enum
}
Fuzzy
something?
protocol FuzzyComparable {
func fuzzyEquals (_ other: Self) -> Bool? // or a tri-state enum
// yes, no, maybe
}
Swift hasn't had an operator added in a long time (for good reason) but if a separate protocol covers fuzzy equality then a default ~==
or ~~
sounds useful.
I don’t think I’d call this “fuzzy” - to me that means something that will indicate two things are equal even if there’s some minor differences (capitalization and Unicode canonicalization with text, etc.). This proposal is talking about pretty much the exact opposite (things that might actually be equal will be indicated as “maybe unequal”), so I’d shy away from “fuzzy” here, either in naming or with the normal meaning of the ~
character in an operator.
Whatever the name ends up being, I’d advocate for a tri-state enum over a Bool?
- when APIs return nil
as a meaningful value, it can result in some programmer logic errors when using optional chaining:
if let maybeEqual = someStruct?.someArray.fastEquals(otherArray) {
…
}
Not that this is the API’s fault, but I’ve seen it enough that I’m wary about APIs that use nil
to return something meaningful and on the same hierarchical semantic level as the value inside the optional, as opposed to falling into an “error-like” category (some input was nil
/empty, in the wrong format, etc.)
Cross posting from https://forums.swift.org/t/how-to-check-two-array-instances-for-identity-equality-in-constant-time/78792/48:
Please let's not expose identities as escapable objects; I fully concur with @Slava_Pestov's note.
I agree there is a legitimate need for quick isIdentical(to:)
checks, and I think it would be a good idea to add such methods as public members on any concrete type where it makes sense: types that implement the copy on write optimization, on referential types like UnsafeBufferPointer
, etc.
SE-0447 has already established the name isIdentical(to:)
. We added these to support simple tests for referential equality without having to conform Span
/RawSpan
to Equatable
. (It would not have been possible to do that, as Equatable currently requires escapability; but even if we fixed that (as we're planning to do), the conformance would be highly ambiguous: some people would read span1 == span2
to compare identities, others would expect it to compare contents. We do intend Span
to conform to a collection-like protocol, and Equatable
currently carries an expectation for elementwise equality for those. It seemed better to avoid this ambiguity, and instead provide clearly named methods for the two different interpretations.)
I don't really see why we would need to define a standard protocol for this, though. (I'd particularly not like to have to think about types like Bool
getting forced to conform to it.) If the protocol has widespread appeal, then it ought to be possible to demonstrate this by shipping it (and building on it) in a package first -- we don't need to immediately jump to defining it in the Standard Library. The package can make use of concrete isIdentical(to:)
implementations in the stdlib to define its own conformances, as it sees fit.
The one mainstream benefit of a separate standard protocol I do see would be that it would allow us to use the ===
operator as an alias for (or in place of) isIdentical(to:)
, including over wrappers like Optional
. Span
/RawSpan
avoided adding any new overloads of ===
to prevent triggering a tsunami of similar overloads for other types, which would be unlikely to scale well.
An important alternative to a new protocol is to add a isKnownIdentical(to:)
requirement to Equatable
, with a default implementation that returns false
.
protocol Equatable {
static func == (lhs: Self, rhs: Self) -> Bool
@available(SwiftStdlib 6.x, *)
func isKnownIdentical(to other: Self) -> Bool
}
extension Equatable {
@inlinable /* @backDeployed etc */
public func isKnownIdentical(to other: Self) -> Bool { false }
}
This avoids the (mostly mental) overhead of a whole new protocol, but it does put such an operation into the member namespace of every equatable type, which is quite noisy -- so this too would be best proposed once there is widespread agreement that we need to routinely invoke such operations from generic contexts. (Note that default implementations for protocol requirements tend to get compiled into Swift binaries as part of specialization/inlining, and so it will lead to components of an application disagreeing on the return value of isKnownIdentical
. This is not necessarily a problem, but it is a complication.)
As of right now I'm still thinking this work belongs not strongly coupled to Equatable
. That means we don't add new methods on Equatable
and it means we don't ship a new protocol that itself adopts Equatable
. My preference so far is that Distinguishable
could be adopted on a type that is not Equatable
and that should be ok and supported.
This looks like we're back to the isIdentical
pattern. Except of returning a Bool
where true
indicates "must be equal" and false
indicates "might not be equal" we define an extra type. I still feel like the existing precedent on isIdentical
already shipping in standard library should lead us to prefer isIdentical
.
Did you discover this "maybe equal" operation in any alternate languages or ecosystems? Did you find any "prior art" examples that could help us see how other languages or ecosystems express this idea?
@lorentey I didn't follow along too closely with the Span
review while it was taking place… but I just went back and searched through the pitches and reviews and I did not see a mention of the isIdentical
function being discussed. Do you remember if there were any offline discussions about why this function should be named isIdentical
? Were there any offline discussions about a potential mustBeEqual
function instead?
Hmm… so the thinking here is we could go ahead and ship public isIdentical
implementations on the appropriate standard library and Foundation types and then ship a potential Distinguishable
protocol somewhere like swift-collections
? And when a product engineer depends on swift-collections
they get the Distinguishable
protocol available for their own types as well as the adopting on types like Array
and TreeDictionary
?
I do believe there are legit generic context use-cases inside the standard library for us to optimize if Distinguishable
shipped in there. It would be impactful to ship them sooner rather than later. I guess if we did ship in swift-collections
and then incubate for one Swift release cycle that would put us about another six months in the future?
Hold on—why is this not just the existing protocol Identifiable
, and the corresponding operation just foo.id == bar.id
? The semantics for Identifiable
state:
Identifiable
leaves the duration and scope of the identity unspecified. Identities can have any of the following characteristics:
- [...] Unique within the current collection, like collection indices.
If something like a collection index, which can change on mutation, is considered an acceptable "duration and scope" of identity, so too would be the address and count of a Span
, etc. These types could vend an opaque, unconstructible ID—maybe even noncopyable and nonescapable if we can retrofit it.
IMHO Distinguishable
should refine Equatable
(i.e. if you implement isIdentical(to:)
, you should also implement ==
.) But I can buy completely decoupling them. I don't think isIdentical(to:)
should be part of Equatable
, especially if the default implementation is effectively useless, since Equatable
is a very common conformance but the implementation of isIdentical(to:)
likely will need special-casing.
See my "compare all the bits" implementation in the other thread—it's provably correct for copyable types and reference types, but still kind of useless.
Silly question, but since namespace pollution of adding an instance member to Equatable
seems to be a problem big enough (and I agree!) that people are now discussing adding a new protocol for identity comparisons…
Was it considered to add a static
defaulted member to Equatable
instead? There's a pretty good naming candidate too for that member, too[1]:
protocol Equatable {
static func == (lhs: Self, rhs: Self) -> Bool
static func === (lhs: Self, rhs: Self) -> Bool
}
extension Equatable { // default implementation
static func === (_: Self, _: Self) -> Bool { false }
}
(I suppose some work is needed to make the AnyObject
comparing ===
overload take precedence, but I guess it can be done.)
Edit: If adding ===
to Equatable
is a nonstarter, I think the operator spelling would be a sensible protocol requirement for a protocol Distinguishable
refining Equatable
rather than a member function.
I think
===
was suggested as an alias forisIdentical(to:)
, but why need both? ↩︎
Well… this kind of breaks down when I think about how to make practical use of Distinguishable
. Just from referencing the example from Identifiable
I can see how we are mapping to some ideas that might not work well together:
If I would expect that a type that conforms to Distinguishable & Equatable
and returns true
for isIdentical
should imply value equality between those two instances… that kind of breaks down when we consider that id
should be stable even if other data fields change. Otherwise we would have a situation where two instances either return "the same" id
for a false-positive when value equality returns false or return a different id
and indicate this is an entirely new and different user
. And neither of those outcomes really fit with what I was thinking for Distinguishable
.
Other than a subjective judgement about whether or not Distinguishable
should refine Equatable
as a matter of style… I do believe we can also consider the existing examples of Span
and RawSpan
as legit use-cases for types that choose to publish some concept of "identity equality" while explicitly opting out of publishing value equality. If we published Distinguishable
as a refinement on Equatable
that would seem to block us on adopting Distinguishable
on Span
and RawSpan
. It might then look ambiguous to product engineers why Span
and RawSpan
implement isIdentical
but don't adopt Distinguishable
.
I do expect most engineers putting Distinguishable
on their types also want value equality… I'm just not completely clear it is right to make that decision for them. If an engineer wants to publish identity equality without value equality then I'm thinking that should be supported.
There's also the question about what to do with AnyObject
. We might choose to discuss adopting Distinguishable
on AnyObject
with isIdentical
just forwarding to ===
. But we lose that option if we then also require Equatable
on Distinguishable
.
FWIW my POV is that a more primary driver for an independent protocol would be that Distinguishable
itself might not need to map directly to any ability to determine value-equality. Keeping them independent offers more flexibility to the product engineer that chooses to adopt it.
I don't really think this can counts as a precedent since _isIdentical
is a hidden API.
Only other languages with copy-on-write containers would qualify for having such an operation. And then only if they don't expose a pointer to the storage (because then you can compare the pointer instead of having an opaque operation for doing that comparison). I don't really know any I could draw inspiration from.
Container types in Swift hide the fact they have an identity. They don't expose a reference, they do not conform to Identifiable
, they always appear to be distinct values thanks to copy-on-write. Except in a few edge cases, the storage's identity is purely a detail of the implementation.
But do we really want to reveal those types have an identity in the first place? And if they have an identity, how does it relate to Identifiable
? Is it some sort of SecretIdentifiable
, where we can compare identities without exposing the identity itself? Or is Identifiable
a higher level of identity than we're looking for here?
Does this protocol actually have anything to do with the storage identity? Is it valid for Int
to conform to Distinguishable
? What about ContainerOfOne
or InlineArray
? I'm starting to think the problem with this proposal is that it lacks guidance about when it is appropriate or not to apply Distinguishable
to a type.
The stated goal is "determining if two instances are distinguishable in constant-time." That's easy enough to do with Int
, so it could conform. But if by "instance" we imply there is some out-of-line storage in this type, this is certainly not the case for Int
. Could be the case for BigInt
though.
And then String
or BigInt
could store small values inline, so sometime there is no storage identity. The implementation of _isIdentical
in String
says that in this case we treat the content of the inline storage as the identity for comparaison. This is perfect for the purpose of "determining if two instances are distinguishable in constant-time", but it also implies it's reasonable for Int
to conform. Is this the intent here?
I feel like theoretically this protocol could be applied to every Copyable
type in Swift and isIdentical
could just return raw byte equality[1].
Maybe the intent is to only apply it to types which don't have constant-time equality (so as to signal the availability of a shortcut). But then it ties the protocol to the concept of equality. Tying it to equality could have funny effects in some cases (like Float
[2]), so maybe this is a good reason not to tie this to equality.
I was trying to suggest a rename of isIdentical
to something clearer based on how it should be used rather than something telling us a characteristic of the type. But maybe I was doing this because I can't see clearly what you're trying to express here. Either this is a fundamental trait available to pretty much every type, or it is a practical concept for types where it makes sense to test for a shortcut path. I feel like you're naming it as the former while using and implementing it as the later, hence my confusion.
Maybe it breaks in the presence of weak references. If not for that, I think it would make sense to tie
isIdentical
toCopyable
and define it as meaning bit-wise identical. Perhaps there's a way to define a kind of bit-wise identity that would work around weak references? ↩︎Float
is a strange case because nan and nan could be bit-wise identical but still would not be equal. Two nans are also not necessarily bit-wise identical because they could carry a different payload. And while-0
and+0
are indeed equal, they are not bit-wise identical. (This equality weirdness propagates to equality evaluation of container types, such asArray<Float>
.) ↩︎
Well… String._isIdentical
is public
. Underscored… but public
. I can download a production Swift toolchain and it's there and available for me to use.
I can't see the rdar://104828814 that was referenced to land 63307… but if anyone can open that up you might have some more context how and where this might be used.
Here's a thought experiment… suppose we weren't talking about landing any protocol. Suppose all I was pitching was that the String._isIdentical
function should become a legit supported API without an underscore. What would your feedback be on that pitch? Would your feedback on String.isIdentical
be the same as your feedback here? Again… assuming there was no Distinguishable
protocol being pitched.
There's additional precedent shipping in 6.2: Span
and RawSpan
. Both those types ship an isIdentical
function without adopting Equatable
. Did you participate in the SE-0447 design review of Span
and RawSpan
? What was your feedback when the isIdentical
function was proposed? Was it the same as your feedback here? If you didn't participate in the review of SE-0447, and you had the opportunity to "retroactively" give feedback, would that be the same as your feedback here? Would you suggest, now that these types have already been through design review and landed, that isIdentical
should be renamed to something else? Would you suggest that isIdentical
is somehow not communicating well enough?
Suppose, in addition to making isIdentical
official and supported on String
, I also proposed isIdentical
on Array
and Dictionary
. And Set
and Data
. And all the rest of the VIP CoW types in Standard Library and Foundation. But without any Distinguishable
protocol being proposed. So the pitch was only adding the new isIdentical
on some set of concrete types. What would your feedback be on that pitch? Would that be the same as your feedback here?
What this all leads to is it's possible that Distinguishable
might actually ship in a package (following a suggestion from @lorentey) If we for now decoupled the idea of some protocol away from the idea of what these functions on these concrete types are called… does that change your feedback?
Oh we had exhaustive arguments on every single aspect. The post I wrote already summarized what I remember discussing.
- Checking that two
Span
instances are addressing the same memory is a basic need that is not readily implementable outside the stdlib (unlike with UBP). (Checking whether one span is contained in another is a similarly important need, and that opened even more discussions.) - Adding an
===
overload would be the obvious move, but it seemed unlikely it'd be a good idea to start defining ad hoc operator overloads in Swift (at least not on the level of the stdlib). - Defining
==
to compare referential identities would be incredibly confusing/misleading, as people have come to expect that a container type (likeSpan
wants to be) would implement its equality test by comparing contents. - For a number of years we've already been using the
isIdentical(to:)
name for exactly this purpose, including in stdlib internals and in public API inside packages like swift-collections. It seems like as straightforward/boring/obvious a name as they come -- I don't remember anyone misunderstanding it to mean anything other than what it is. - The one notable drawback (of both
isIdentical(to:)
and===
) is that it is quite verbose to test if two optionals contain identical spans, unless we also add extra helpers on Optional itself. Luckily, Optional did not support nonescapable wrapped types at the time Span was proposed; it does now, but this still seems a minor wrinkle -- we have some far more fundamental usability issues with nonescapable/noncopyable optionals.
These are just the concerns I remember discussing with Guillaume; while I've been quite shamelessly exploiting that we're working within shouting distance, he was also receiving useful feedback from others, including some folks much higher up in the pecking order than me.
Perhaps I've missed some earlier posts on this topic, but this is the first time I see mention of a mustBeEqual
function; for what it's worth, I have a difficult time figuring out what it would do, based on just its name. To me, it reads like it states a requirement, which gives me the impression it might be for doing nonstandard precondition checks.
For what it's worth, I'm fully on board with the first half of this! (I'm willing to help push for it. Caveat: it isn't clear if that will help or hinder the desired outcome.)
I am also willing to work on shipping any missing isIdentical(to:)
methods on types defined by swift-collections, provided the stdlib ends up adding the prerequisite dependencies. (E.g., OrderedSet.isIdentical
would currently need to forward to Array.isIdentical
.)
I'm not fully ready to sign up swift-collections to ship a protocol like Distinguishable
though -- I need more convincing that (1) it would fit within the mandate of that package, and (2) that it would actually carry its weight.
I really think protocols are best started as implementation details in code that needs them. swift-collection itself has already gathered a small selection of "marker-like" protocols (_SortedCollection
and _UniqueCollection
) that help it implement generic fast paths, similar to the niche that Distinguishable
might want to fill for equality checks. In principle, introducing protocols that express static guarantees about the shapes of collection types seems like a good fit for that package; but so far it didn't seem necessary to expose these helpers, and their particulars in fact make it safer for them to remain internal indefinitely -- making them public just on the off chance someone might find a use for them tends not to work well.
(Note: This line of objection would quickly melt away if some high-profile ABI stable codebase like SwiftUI had a good reason to want fast generic identity tests -- but in that case, I think extending Equatable
is the far better option -- see below.)
Returning the abstract "identity" of a container as a value in its own right exposes information about implementation details that do not need to be exposed, and are not worth exposing.
I am fully on board with the proposition that people sometimes do have a legitimate need for a way to quickly test if two containers are entirely identical without going through the a full equality test. (I regularly need that myself.) This is merely asking for a narrow performance hook -- an entirely reasonable request, with very few opportunities for abuse or unforeseen consequences. We can easily satisfy this need by simply adding a single member function. (If it turns out we also need to allow doing this shortcuts in contexts that are generic over Equatable
, then the most direct way to support that is to bolt a new requirement directly onto Equatable
.)
There is no need to go overboard and invent the notion of an abstract identity, decoupled from the object it identifies. Abusing the Identifiable
protocol would be even worse -- the purpose of that protocol is to allow reasonably efficient identifier-based lookups (such as while calculating the difference between two snapshots of a model state), by formalizing the idea of hashable user-level identifiers (such as UUIDs, serial numbers, or database keys), to allow their use as keys in Dictionary
or Set
. I think it would be a Bad Idea to encourage people to do that sort of thing with the "identity" of an Array
value.
Identifiable
allows (and encourages) conformer types to choose whatever hashable Identifier
type they prefer. In general this makes it tricky to optimize code that uses it, by forcing the var identity
getters and the ==
call to separately go through non-inlinable/non-optimizable dynamic dispatch through some witness tables. This is actively harming the "quick shortcut to ==
" use case that the pitch is set out to solve. (It trades one opaque isIdentical(to:)
function call that returns a simple Bool
for three separate calls that return and take some opaque identifier type. That is not a good trade to make if we're trying to speed things up.)
Array
is expressly designed not to expose an identity for itself -- it prefers to pretend it is a simple value type. By only adding a narrow isIdentical(to:)
function, we'd be chipping away at this illusion, but we wouldn't completely shattering it.
Exposing a fully formed identity raises too many questions for comfort:
- Will people expect that identifier to change whenever the container is mutated? What if some container types did that, and people started assuming that it is a general requirement?
- Will people expect that the identifier only changes when the container is mutated in a way that makes it reallocate its storage? (Again, what if some containers did that, and people start assuming that is a general requirement?)
- Will people expect that identifiers never get reused, even if a container gets destroyed? (That has been a constant issue with
ObjectIdentifier
, despite the docs.)
To properly design a container identifier, we'd need to think about such questions, and some poor soul will likely have to write documentation on the recommended design patterns for such types. I believe this would be completely unnecessary work. We don't need to go overcomplicate this like that -- the actual problem doesn't need it.
Assuming there is a legitimate, real need to run isIdentical(to:)
in generic contexts, then I do suspect that the most efficient and most practical way to support that is to add opt-in identity tests directly as a new requirement to Equatable
. (This opinion is not strong, because I'm not fully convinced that the premise is true yet.)
I feel that proposed solution of defining algorithms that explicitly require Distinguishable
conformances unnecessarily pushes an incidental implementation shortcut (whether the algorithm wants to implement a fast path for identical objects) into the public API surface.
The approach being pitched is this:
public protocol Distinguishable {
func isIdentical(to other: Self) -> Bool
}
func f3<S>(sequence: S) async throws
where S: AsyncSequence, S.Element: Distinguishable
{
var oldElement: S.Element?
for try await element in sequence {
if oldElement.isIdentical(to: element) { continue } // Quick shortcut
oldElement = element
doLinearOperation(with: element)
}
}
Note that Distinguishable
is part of the algorithm's requirements; we cannot call this function on anything that doesn't conform to this niche protocol. To support calling it on classic Equatable elements, we'd need to add an overload that falls back to that, and a third overload if we also want to support types that conform to both. I don't think there is a reasonable way to merge the Equatable
and Distinguishable
variants into a single function; the only way I can see to do that is by doing runtime as?
downcasts, and doing that would generally be costly enough not to be worth the effort of implementing the shortcut in the first place.
As a library author, I would not love if I was forced to add a bunch of overloads to my public API only to add a trivial fast path over ==
. And as an author of copy-on-write Equatable
types, I already routinely implement ==
to start with a trivial identity test; pasting that code into a new method does not require much effort from me. As a user of Swift libraries, I prefer not to have to wade through a myriad overloads of the same basic function to find the variant I'm looking for.
It would be far less mentally taxing on me (in all three hats) if we could simply go with a new requirement on Equatable
:
public protocol Equatable {
static func ==(left: Self, right: Self) -> Bool
func isKnownIdentical(to other: Self) -> Bool
}
extension Equatable {
func isKnownIdentical(to other: Self) -> Bool { false }
}
func f3<S>(sequence: S) async throws
where S: AsyncSequence, S.Element: Equatable
{
var oldElement: S.Element?
for try await element in sequence {
if oldElement.isKnownIdentical(to: element) { continue }
oldElement = element
doLinearOperation(with: element)
}
}
There are two drawbacks I can see: (1) adding a new requirement to a commonly conformed protocol Equatable
would ever so slightly increase the overall code size of Swift binaries with Equatable conformances (i.e., most binaries), and (probably much more importantly) (2) the new requirement would get added to the member namespace of every type that conforms to Equatable, including ones like Bool
where its semantics don't really make sense.
The second issue may be somewhat mitigated by taking inspiration from Jordan, and making the new requirement return a tristate bool:
public protocol Equatable {
static func ==(left: Self, right: Self) -> Bool
// Returns if `self` can be quickly determined to be identical to `other`.
//
// - A `nil` result indicates that the type does not implement a fast test for
// this condition, and that it only provides the full `==` implementation.
// - A `true` result indicates that the two values are definitely identical
// (for example, they might share their hidden reference to the same
// storage representation). By reflexivity, `==` is guaranteed to return
// `true` in this case.
// - A `false` result indicates that the two values aren't identical. Their
// contents may or may not still compare equal in this case.
//
// Complexity: O(1).
func isKnownIdentical(to other: Self) -> Bool?
}
extension Equatable {
func isKnownIdentical(to other: Self) -> Bool? { nil }
}
Note how this differs from Jordan's design in the meaning of false
. I intended this modification to resolve some fundamental objections to the tristate variant, but I haven't fully thought through what complications it introduces in their place. I think it's a good idea for a member requirement solve just one problem -- the idea of quickly testing "definitely not equal" is a separate, far more controversial/ambiguous task, and it should not be forced to get tackled by the same member.
I think having a distinct nil
value as default does help by adding a little more sense to having this member on flat Equatable types like Bool
or Int
. The name isKnownIdentical
does not quite match the behavior in this case, but it's pretty close. (isIdenticalIfAvailable
would be quite bad, I think.)
The Standard Library includes ample precedent for adding optional protocol requirements in this nil-returning way. The result is usually not the most user friendly API, but it fits an established pattern, and it helps that (unlike withContiguousStorageIfAvailable
and friends) this isn't a higher-order function:
func f3<S>(sequence: S) async throws
where S: AsyncSequence, S.Element: Equatable
{
var oldElement: S.Element?
for try await element in sequence {
// Is this readable enough? Is it easy to figure out how to write it?
if oldElement.isKnownIdentical(to: element) ?? false { continue }
oldElement = element
doLinearOperation(with: element)
}
}
Ahh… ok… so it sounds like the suggestion then is:
- ship
isIdentical
on concrete types in standard library and foundation - do not ship a standalone
Distinguishable
protocol - potentially add a new function to
Equatable
to expose the identity equality
This makes sense. I can agree with these ideas.
Correct. One of the places I could see this leading to measurable impact is an Observable
type (that is Equatable
and offers identity equality) that then recomputes a SwiftUI component body
. The current plan is to ship a linear-time equality check inside Observable
that filters new values before notifying observers.[1] But this could potentially then make use of identity equality before notifying SwiftUI (which we assume might perform its own linear-time operation when values have changed).
Correct. It gets bulky and clunky. My original thoughts were that increased flexibility from a Distinguishable
independent of Equatable
meant Distinguishable
could be adopted by types like Span
and RawSpan
that do expose identity equality but do not expose value equality. But the argument you are making is those potential wins from the extra flexibility are not worth it when we consider the extra gymnastics and ceremony that would go into using Distinguishable
from generic contexts that might also need to be generic over Equatable
. Makes sense. I can agree with this.
Yeah… I'm agreeing with this.
All good ideas. Thanks!