SE-0261: Identifiable Protocol

identifiedValue was removed because it wasn't clearly intrinsic to the Identifiable concept. An ad-hoc identification mechanism, like SwiftUI's Collection.identified(by:), could be enabled in the future by a general purpose language feature without diluting Identifiable.

In the meantime, a library can manually support ad-hoc identification. For example by declaring a convenience initializer:

struct List {
    public init<Data: Collection, ID: Hashable, RowContent: View>(
        _ data: Data,
        id: KeyPath<Data, ID>,
        @ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
    )
}

Note: if it becomes clear later that identifiedValue does belong on Identifiable, my understanding is it can be added in an ABI stable way because it has a natural default.

1 Like
    • ID is an abbreviation (hence the capitals). cf. US
    • id is a word (hence the lowercase with no trailing dot). cf. us

    In prose, they do not mean the same thing. See the entry in the Oxford Dictionary of English.

  1. It doesn’t matter. The API Design Guidelines explain camel casing of abbreviations and acronyms. (Click “More Detail”.) Both would end up as id at the beginning of an identifier in properly cased Swift code.

  2. ID is like URL or sin(x) in that it is more recognizable than its spelled‐out form, no matter whether you are a grade schooler or a university professor. The API Design Guidelines provide for this sort of thing. (Click “More Detail”)

6 Likes

+1 Identifiable makes more sense as a member of the standard library than it does as part of SwiftUI.

Why must ID be Hashable? I think Equatable is enough for identification. DBs often use trees for indices instead of hash tables. Don't we want to make ID Comparable instead of Hashable in such cases? Even if ID is just Equatable in the standard library, we can make ID Hashable in some module in the following way when we need it.

protocol Identifiable: Swift.Identifiable where ID: Hashable {}

ADDED: Although I have no idea of concrete cases when we do not want ID with Hashable, I think the standard library should be kept minimal. ID with Hashable may be a same way as Java's Object class with the hashCode method.

+1
At first I was thinking that we should also get an overload to === but then realized that it was better to use the equality of id as in
a.id == b.id

3 Likes

If it wouldn‘t be hashable then you can‘t use ID‘s as dictionary keys or put them into a set, equality alone is not enough here. Id‘s say hashability requirement is correct here.

2 Likes

This☝️

I also misunderstood the proposal until I read @Karl's comments.

Sentences like "allocating class instances to represent identity of value types is needlessly costly" made me feel like I completely misunderstood value and reference semantics. The proposal often implies values can have identity, whereas I always understood that lack of identity is their defining property.

After @Karl's clarification that the proposal is (or should be) about record identity, I am +1 on what the proposal is trying to achieve, but -1 on the proposal itself, because it's so confusing.

Please keep in mind that SE proposals are often referred to as documentation, so even if this gets accepted with modifications, the proposal should be updated to include a discussion on record identity vs. reference identity and remove sources of confusion between the two.

4 Likes

+1 for the overall proposal, this is a pattern that I frequently implement in my own code for diffing. Promoting this to the standard library eliminates the need to leak those custom types at module boundaries.

This is likely a familiar concept to those who have written a lot of UI, but I agree that a breakdown of reference/record identity should make it into the proposal.

-0.5 for the default implementation on class types: the convenience is outweighed by the potential for confusion and/or incorrect behavior. One example: many legacy codebases use classes for model objects to allow for objc interop – object identity is clearly the incorrect source for record identity in such a case. I can imagine how frustrating (and non-obvious) it would be to track down bugs when someone inadvertantly uses the default implementation.

1 Like

After working on the internals of Combine I will have to say this functionality is immensely useful. This pattern is something I have used frequently even outside of Combine so it definitely feels appropriate for something living in the standard library.

I do have a few concerns: first off the name of id as the parameter name feels a bit off since that is a keyword in objc. Personally I would rather identifier or something along those lines instead but I am not horrifically offended by that property name being id.

There are a few portions that might be interesting impacts here. In Combine we use a protocol without an associated type which allows existential casts. This can be potentially useful. Is the associated type really that meaningful? I think we could adopt this as is but it would mean that we would have to build the usage of the CombineIdentifier as AnyHashable which seems ok but could have some perf impact. The only major issue is that it would mean it would not be serializable over the wire; we are intending to use the identifiers as a mechanism to hang debugging features off of (and I presume many other products using this might be doing something similar).

There is one gotcha that seems a bit cagey. The protocol allows the id to potentially be re-assigned or re-generated. Consider the following usage:

struct Contact: Identifiable {
    var id: Int { generateID() }
    var name: String
}

That would mean that any access to id would return a new generated identifier. This would probably be really bad. Furthermore the protocol allows the var to be assignable too (which has the same failure mode as returning a hash). These objections should not be considered as a blocking type of objection but more-so something that should be considered imho.

Overall this proposal looks fantastic (I hope we can react to this before everything gets locked down for 5.1 in time!)

This is an issue that comes up a fair amount and it seems like something Swift should handle at the standard library level.

I would have liked this type of functionality at the language level that would extract identity as a feature like structural types or class types but that definitely seems like a larger task. I am not sure this is will or will not work into that direction (which I think is the longer term solution)

I did a pretty thorough read and have implemented something similar in other projects. And this is approximately the same solution I have come up with multiple times.

4 Likes

As an addendum; as it currently stands with an associated type it would not be adoptable by Combine since we require Subscription to be able to be expressed as an existential. As it stands that would be a large barrier to entry.

1 Like

If the same identity can be used to identify different things, what’s the point of identity?

But doesn't the current version of Identifiable available in Xcode 11 have an associated type? Why is that one workable but the one in the proposal isn't? Or did I miss something?

I was speaking of adopting Identifiable as a replacement for CustomCombineIdentifierConvertible; Both SwiftUI and Combine are doing the same type of thing but unfortunately with the associated type it prevents Combine from adopting it.

If the two items are adopters of a common protocol; for example Subscriber from Combine it allows two generic types to be understood to be different items. Also there are cases where identity forwards; such as AnySubscriber will forward its identity to the wrapped item.

Per the concept of pointer re-use; it is definitely an issue, the cost however is that each adopter must have dedicated storage for the identifier and those must also be then generated in a thread safe manner. Depending on the ending use-case that may or may not be meaningful consequences.

I’d be interested in seeing at least a consideration to a design using a concrete type like ValueIdentity similar to ReferenceIdentity. Perhaps ValueIdentity could just use a plain small string internally to make it fast.

An update from the core team on the below would be nice.

1 Like

I know I am leaning on Combine as an example, but it is the most recent for me to make cogent arguments about; but we need to have the ability to have a heterogenous bag of both reference and structural types that can have equitable identity.

protocol Foo: Identifiable { }

struct Bar: Foo { ... }

class Baz: Foo { ... }

func hasBeenSeen<F: Foo>(_ item: F) -> Bool {
   guard !seenItems.contains(item.id) else {
      return true
   }
   seenItems.insert(item.id)
   return false
}

However if you are referring to ObjectIdentifier and a similar ValueIdentifier type thing it would require it to have a concept of a "generate unique" that would be thread safe and non-colliding with pointers (at least for a reasonable range). That is roughly what CombineIdentifier does today; it stores a UInt64 under the hood and the construction gives out an atomically incremented global generation count as the backing storage when you make a new identifier.

Per the self/associated-type issue, I am not sure that is solvable in the timeframe we have to work with (given the proposal is for 5.1). I would love to be wrong on that.

1 Like

Your example needs expanding a little to show a need for an existential-able Identifiable. In that particular snippet you could just constrain F to have ID == Int (or Foo could constraint it) to achieve the desired results.

To clarify that particular example we have APIs that are in the form of func f(_ item: Foo) right now. Particularly the one in question is: func receive(subscription: Subscription).

I was just illustrating in that code the need for both class and struct unification.

Id‘s say hashability requirement is correct here.

IDs are not necessarily used as keys of dictionaries. Identifiable in the standard library can be used in any way. We don't need Hashable when we use IDs as keys of trees (then IDs must be Comparable instead).

So I proposed to make ID conform to just Equatable in the standard library. Even then we can add the requirement to ID to conform to Hashable in a module which need it. For example, we can declare SwiftUI.Identifiable whose ID conform to Hashable in the following way in the SwiftUI module.

public protocol Identifiable: Swift.Identifiable where ID: Hashable {}
1 Like

Do you have a good example where requiring a Hashable identifier would be a burden?