SE-0261: Identifiable Protocol

These are tools. You can use them and you can misuse them. That Identifiable has the potential to be used in a way that confounds the nuances of identity is no different from classes having the potential to be used in lieu of value types, or vice versa.

Identifiable has real value for real problems. Use it when it makes sense. But use something else, when it doesn't.

3 Likes

That's a very good angle at the problem. Taking this pragmatic viewpoint (the standard library provides a set of tools, use them wisely), I'd change my overall evaluation to +1.

3 Likes

For the mental model of identity, it helps me to think about Entity-Component-System frameworks. Components hold some state of an Entity - either as structs or classes. An Entity itself can also be modeled as either struct or class. If it is a struct then it needs a unique value in some domain - maybe an index into the list of all Entities or a UUID. If it is a class, the address of the object might serve that purpose.

If a Component would adopt Identifiable then id should return the identity of the Entity, even if it is an instance of a class.

To reiterate: this has literally nothing to do with value vs. reference types. Structs and enums are great, classes are great, and it's all totally irrelevant to this topic. Even classes can have a separate notion of record identity which transcends their reference identity/memory address (see CoreData, NSManagedObjectID).

Essentially, this conformance communicates that a value is part of a larger, non-trivial dataset, e.g. one contact in a database of contacts. Other kinds of types (like, say, FileManager) don't have a concept of record identity because they are not elements of any meaningful higher-order container like a table or graph.

I am disappointed that the proposal text has not been changed to clarify this. It has led to lots of confusion in this review.

6 Likes

How long does combine need to store the bag of seenItems identifiers? Is it possible for these identifiers to outlive all Combine-owned copies of the value (possibly an object reference) that vended the identifier? Or does Combine always store a copy of the value at least as long as it stores the identifier in seenItems?

I asked earlier but didn't get an answer. What is the reason this API uses an existential instead of a generic constraint?

I'm coming a bit late to the heavy discussion above about naming conflicts with the id property, but I had some related thoughts that might make the idea more palatable, with a little bit more help from the language.

I agree with the folks who think that it's a non-goal to try to come up with a name for this property that isn't going to collide with someone's existing code. While it's nice if that can be done, the overall design and readability of the protocol shouldn't have to suffer by being made more obfuscated.

The Swift language has an internal attribute that almost lets us have the best of both worlds: @_implements lets you declare that a property, method, or associated type implements a particular requirement of a protocol even if it has a different name (this is similar to C#'s explicit interface implementation concept).

Right now, there's just one problem: if that other name still happens to be an existing declaration on the conforming type, you end up with an ambiguity:

protocol Identifiable {
  var id: String { get }
}

struct Record {
  var id: String { return "Record.id" }
}

extension Record: Identifiable {
  @_implements(Identifiable, id)
  var idForIdentifiable: String { return "Record(Identifiable).id" }
}

let r = Record()
print(r.id)  // Desired: "Record.id", but error below 🙁 

let i: Identifiable = r
print(i.id)  // Desired: "Record(Identifiable).id"
main.swift:15:7: error: ambiguous use of 'id'
print(r.id)
      ^
main.swift:6:7: note: found this candidate
  var id: String { return "Record.id" }
      ^
main.swift:11:7: note: found this candidate
  var idForIdentifiable: String { return "Record(Identifiable).id" }

I would propose two things (which certainly shouldn't be combined with this proposal, but which offer a path that may ease the concerns in the discussion above):

  1. @_implements should be made public.
  2. Modify the behavior of @_implements to remove the ambiguity; in the example above if you refer to id on an instance of the concrete type Record, then it would only refer to the concrete type's property and not the renamed protocol requirement.
8 Likes

Yeah, a couple years ago on the mailing list there was also chatter of syntax like var Identifiable.id: Int floating around that would achieve the same result.

There are clearly ways that, long-term, property name collisions could be disambiguated at the language level. I think using id is fine as proposed.

It's probably not the first occurrence, but here's one mention I remember:


(even that is very outdated, though :-)

I'd favor the name that is most likely to clash with existing code (that's the best one ;-) — and if we would ever get a way to explicitly refer to a protocol member, that could resolve the conflict... but this feature wouldn't be for free: Suddenly, calling id on an object could produce different results, depending on the context.

Update: The syntax might be reusable for How to unambiguously refer to a symbol defined in an extension in third party module?

It would be cool to consider an implementation that doesnt require to initialize the id value when initializing the ‘Identifiable’ eg make id optional, this would also make the proposal fit more server side swift applications. For example Vapor has this implemented in a similar fashion in their Fluent package. Where the object has an optional property id and also implement a function ‘requireID()’ that throws when id is nil. (Ids are initialized when the object is saved to a database for example) this works quite nice and allows users to either unwrap the optional themselves or call a throwable function.

The proposal supports conformances where typealias ID = UUID? and similar where the identifier itself is Optional. If you want your identifier to be optional you can do that. It would be undesirable to define the protocol with var id: ID? because then it would not be possible to have a non-Optional id property.

5 Likes

Ah totally overlooked that possibility, awesome.

Personally, I was surprised to see a hardcoded id variable as part of the Identifiable protocol. I much prefer the flexibility offered by Paul Hudson's approach. He uses a KeyPath for Identifiable conformance, offering flexibility around the name of the identifier. For a Person, we could use a variable called ssn instead of id, and for a Book we could use isbn. Here's the example from his approach:

protocol Identifiable {
    associatedtype ID
    static var idKey: WritableKeyPath<Self, ID> { get }
}

This could also prevent having to write some computed properties just for the sake of Identifiable. In situations where the id property could cause a conflict, a developer would now have more options.

I understand this could hurt the learning of Identifiable, as developers will need to learn about KeyPaths in order to start using it. Are there some reasonable defaults we can provide to decrease the knowledge space but still leverage this flexibility?

8 Likes

I really like this.

protocol Identifiable {
 associatedtype ID: Hashable
 static var idKey: WritableKeyPath<Self, ID> { get }
}
extension Identifiable {
 func idKey() -> ID {
    return self[keyPath: Self.idKey]
 }
}

struct Person: Identifiable {
 static let idKey = \Person.socialSecurityNumber
 var socialSecurityNumber: String
 var name: String
}
let taylor = Person(socialSecurityNumber: "555-55-5555", name: "Taylor Swift")
print(taylor.idKey().hashValue)

1 Like

Why is the key path approach preferable to this?

struct Record {
  var uuid: UUID
}

extension Record: Identifiable {
  var id: UUID { uuid }
}

This seems much more straightforward and understandable to me than using a key path for indirection.

12 Likes

If we went with KeyPaths, I would prefer if the property was named primaryKey (or, perhaps more controversially, just the key).

This proposal is defintely getting fileprivate'd, :( K.I.S.S. and +1 to the existing proposal as is.

3 Likes

By using a KeyPath, the id can be named anything the developer chooses and pointed to using a KeyPath. This avoids having two properties like in your example. Should your code use the uuid property or the id property? This could lead to a fragmented code base where sometimes the id property is used and other times the uuid property is used.

Your given example using KeyPaths could be written as below:

struct Record: Identifiable {
  static let idKey = \Record.uuid
  var uuid: UUID
}
4 Likes

It doesn't avoid having two properties at all, it just makes the second property a confusing keypath. And, in fact, it forces you to have two properties instead of one in the common case where you're happy to just name your ID id.

I really don't like the concept of having all this indirection and confusion just to avoid settling on a name. Would Hashable be better if used a keypath to find an arbitrary property instead of just having hashValue? Should every standard library protocol introduce some form of indirection for its properties and functions?

12 Likes

After thinking about this further, I'm hugely against this proposal.

  1. The default implementation is a loaded weapon set to backfire. It does not take the lifetime semantics of the id values into account, and is just as likely to be wrong about that instead of right. The id might need to be globally unique during the current program execution, or it might need to be globally unique across executions. (This could be fixed by not having a default implementation.)

  2. Putting Identifiable in the standard library invites code to use it, of course. It's trivially easy to imagine (say) two 3rd-party libraries, each requiring Identifiable conformity for the same objects or values, that impose different conformance requirements (such as different associated types, or incompatible identity rules or lifetimes).

    Once there are two pieces of code that impose different conformance requirements, the usability of Identifiable breaks down. If those two pieces of code are different 3rd party libraries, the libraries become irretrievably incompatible.

IMO, the dangers of blessing Identifiable as a unique standard far outweigh the benefits.

1 Like

This would be an argument against including any protocols in the standard library. Vending a conformance to a protocol you don’t own by a type you don’t own is not supported and is liable to break at any time. It’s not currently forbidden by the compiler, but really at some point it should be at least a warning.

2 Likes
Terms of Service

Privacy Policy

Cookie Policy