SE-0261: Identifiable Protocol

ObjectIdentifier isn't supposed to outlive the related class. It's suppose to be a relatively short-lived lightweight instance.

The proposing Identifier is tricky though. It seems to be a long-lived object that can (indirectly) refer back to related instance. And construct the said instance if it's not there.

1 Like

Quoting an unanswered question from the pitch thread.

As convenient as the ObjectIdentifier extension is for classes I think it would be also great if we add a default using Never which letβ€˜s us express that a certain type never can be identifiable.

extension Identifiable where ID == Never {
  public var id: ID { fatalError("Some good description") }
} 

Other than that I support this proposal and think that it fits very well into Swift and stdlib.

We should also kick off a follow up proposal soon to decide which stdlib types will conform to that type (Never, Optional, etc.).

+1

Thanks.

+1 for the concept in general, -1 for the default use of ObjectIdentifer, for the same reasons others have already brought up. I also agree that clarification of the intended conceptual lifetime of a given Identifier would be very desirable - the example that comes to mind is that of a database ORM, where the "unique identifier" (row ID or similar concept) of a given model can obviously outlive not only the local snapshot of the model but the process itself.

9 Likes

An observation and a request: lowercase "id" and uppercase "ID" are very different words. The former means "the part of the mind in which innate instinctive impulses and primary processes are manifest" whereas "ID" is short for "identification" or "identity". Please consider renaming "id" in the protocol to something else (like "identity") or uppercase the name ("ID"). Thanks!

2 Likes

Just thinking out loud, maybe to protocol should require the type to provide an Identify view instead of an identifier.

protocol Identifiable {
    associatedtype Identity: Hashable
    var identity: Identity { get }
}

To me this feels similar to how collections have all its own Index types. This leave a little room for types to still have a unique identifier property if they need it.

What is your evaluation of the proposal?

+0.5. I think the protocol is poorly named and documented.

The thing is, there are lots of kinds of identity. The thing I think we're talking about here is "record identity" - i.e. that two objects may have different values and not be substitutable, but conceptually both talk about the same element of data, like the primary-key in a database. Another kind of identity is object/reference identity.

The name "Identifiable" doesn't really make clear what kind of identity is being talked about, and the default implementation for classes confuses the two concepts. Even for classes, they might not be the same.

Take CoreData, for example: the kind of identity we are talking about here is represented by the NSManagedObject.objectID property (see the docs for NSManagedObjectID). Within an NSManagedObjectContext, there is only one reference to a record, so the two concepts are equivalent, but across contexts, you will see different instances, with different memory addresses, referring to the same record and sharing NSManagedObjectID values. The MOID is used for exactly the kind of diffing described in this proposal.

For this reason, I don't think the === operator needs to consider these values, and I would remove the default implementation for classes. I think that would be confusing, and after all, it isn't much work to write x.id == y.id.

Some alternative names: Record, IdentifiableRecord... something like that, perhaps.

Is the problem being addressed significant enough to warrant a change to Swift?
Does this proposal fit well with the feel and direction of Swift?

Myeh. Kinda. You see this concept quite often in model code, which you often want to share across platforms, so I can see the benefit to sinking it to a lower level. I'm not sure if it should be in the standard library or another library.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

See discussion above about CoreData.

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Quick reading.

EDIT: Oh, and just a suggestion - I think it wouldn't hurt if the Apple folks asked the CoreData team for their input on this (if they didn't already). It seems natural that we would want NSManagedObject to conform to this protocol. I'm kind of surprised that it doesn't already conform to the thing that exists in SwiftUI.

9 Likes

Lowercase "id" has several meanings. Why would you assume one from the fields of psychology or epistemology, when the context clearly suggests the term of art from computing and information systems?

Lowercase "id" is obviously the correct spelling for anyone who has ever worked with collections of data.

9 Likes

Simple +1. Hope we can get this in for 5.1 and have SwiftUI use the Standard Library version.

The Swift Style Guidelines don't explicitly discuss initialisms or acronyms as far as I can tell, but it does encourage one to "avoid abbreviations".

"id" in this context is an abbreviation.

I only mentioned the psychoanalysis sense of the word "id" because that is the first entry in the dictionary[1] included with macOS, iOS, etc. More so, the "ID" abbreviation is the second to last entry in the OED for "id". (The last entry is the official postal code for Idaho.)

[1] – The default dictionary is the Oxford English Dictionary, which is the gold standard for English dictionaries.

2 Likes

Swift is a programming language, not a natural language. The amount of prior art is so vast that this objection comes across as sarcastic. The meaning is obvious to every single person reading a type definition (unless that type happens to be called Mind, and also feature members named ego etc)

We also deal with self and Self without issue.

11 Likes

So, your Account class is broken and must not rely on the default behavior.

Here again, your design is broken if it relies on identity to cache content that is not tight to it. If you want to detect content change using only the identifier, your object must reflect that in the identifier and provide its own implementation.

The Identifier protocol is not related to Equatable. It is supposed to be used to detect if 2 objects represent the same data, that does not imply that they are equals. You may have old data and new data representation that are different, but still represent the same object.

1 Like

That's the point. It's easy to miss the restriction and use the default implementation to have it automagically work. Either the restriction is under-documented, or we need to lay some groundwork of identity semantic.

5 Likes

I think pretty much all of the reviews so far have entirely misunderstood the proposal.

Reference identity has nothing to do with record identity. As you noticed, the operating system might decide to reuse a previous allocation and memory address, but nothing about that implies that the new object refers to the same element of data as the old one.

As I said, we should remove the default implementation for classes. It's cute, but it's also just plain wrong. The identifier is a property of the data itself (i.e. a primary key), not the object which encapsulates it.

14 Likes

It was a pretty roundabout discussion, but yeah. That.

1 Like

I think adding this to the standard library is a good idea. But some steps should be taken to avoid the confusion between object identity and record identity.

We should at least rename the identity operators to "object identity operators" to help differentiate the concepts. (This changes only the documentation.)

It'd be nice if this leads us to getting "record identity comparison" operators. Not only people looking at the operators table would end up understanding the two identity concepts are different things, it'd also be convenient when working with collections:

a == b // value equality
a === b // object identity
a ==== b // record identity

collectionA.difference(from: colllectionB, by: ====)
collectionA.startsWith(colllectionB, by: ====)

What is your evaluation of the proposal?

We definitely need something like this.

I don't like the abbreviation of id and ID. I prefer to use identifier and Identifier respectively.
The other problem I see is that id is required to be a property on the type itself.
The identifier is not always known at initialisation time.
For example if we want to create a new Contact and add it to the Database. The Database will create an identifier for us after we have send a Contact.
In the SwiftUI use case, a detail view for a Contact is most likely not interest in the id of a Contact. It does not display it.
If we want to create a view that allows the user to create a new Contact the view does also not know the identifier of the contact, because it is not yet created.

If the id property is directly on the type we have to make the identifier optional, give it a default value or create an extra type for these use cases.

Is the problem being addressed significant enough to warrant a change to Swift?

Yes. I'm using similar types in my projects as well.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

I'm using 2 protocols and 2 structs to make it more flexible:

protocol IdentifiableType {
    associatedtype RawIdentifier
    typealias Boxed = IdentifierBox<Self, Self>
    typealias Identifier = TypedIdentifier<Self>
}

protocol Identifiable {
    associatedtype IdentifingType: IdentifiableType
    var identifier: IdentifingType.Identifier { get }
}

struct TypedIdentifier<IdentifingType: IdentifiableType> {
    let rawIdentifier: IdentifingType.RawIdentifier
}

struct IdentifierBox<IdentifingType: IdentifiableType, Value>: Identifiable {
    let identifier: IdentifingType.Identifier
    var value: Value
}

This allows to define a type that contains the identifier as a property:

struct Contact: IdentifiableType, Identifiable {
    typealias RawIdentifier = Int
    let identifier: Identifier
    let name: String
}

but also to define a type that only states that it is a identifiable type:

struct Contact: IdentifiableType {
    typealias RawIdentifier = Int
    let name: String
}

In the second case the value can be combined with an identifier:

Contact.Boxed(identifier: .init(rawIdentifier: 1),
              value: Contact(name: "John Appleseed"))

By adding a convenience extension to the IdentifiableType the syntax gets a lot nicer:

extension IdentifiableType {
    func identified(by rawIdentifier: RawIdentifier) -> Boxed {
        return .init(identifier: .init(rawValue: rawIdentifier),
                     value: self)
    }
}
Contact(name: "John Appleseed")
    .identified(by: 1)

Pros:

  • Generic Algorithms over Identifiable work on the type itself if the type has a identifier property, over a boxed value IdentifierBox or even on a TypedIdentifier alone
  • Extra type safety through TypedIdentifier (Inspired by John Sundell: Type-safe identifiers in Swift | Swift by Sundell)
  • Optional separation of the identifier from the value by using IdentiferBox
  • If the type does not include the identifier property the synthesised implementation of Equatable and Hashable is more useful in my opinion, especially in testing.

Cons:

  • More types and a more complex design
  • I'm not happy with the naming of all types, suggestions welcome

The complete implementation includes many additional conformances (Hashable, Codable, etc.) and connivence initialisers to overcome the added complexity of TypedIdentifier, especially for testing.
Initially I had concerns that the TypedIdentifer with its generic parameter would show up a lot in function signatures but I found that in my codebase I only use the type alias on the type like Contact.Identifier or Contact.Boxed.

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

in-depth study

Could someone perhaps explain how the protocol proposed here relates to the SwiftUI.Identifiable protocol? The latter has an additional property

    /// The value identified by `id`.
    ///
    /// By default this returns `self`.
    var identifiedValue: Self.IdentifiedValue { get }

Isn't it unfortunate to have two (slightly different) protocols with the same name in Swift and SwiftUI?

2 Likes

The idea is that SwiftUI will be modified to use the Identifiable from the standard library instead of defining its own, check out the pitch thread that came before this proposal: Move SwiftUI's Identifiable protocol and related types into the standard library

1 Like

NSManagedObjectID "solves" this by abstraction and introducing the concept of temporary IDs. Once the object is saved it gets a permanent ID.

You have two options in this case:

  1. Keep both temporary and permanent IDs and implement == to first check if the temporary IDs match. You might run in to issues with hashing and substitutability, though.
  2. Only keep one ID and model object creation as if the temporary object was removed and the permanent one was inserted.

There isn't really a perfect solution to this problem. It's been a while since I really used CoreData, but I think it takes the latter approach and it works fine in practice.