SE-0261: Identifiable Protocol

Would it actually make any sense to provide an initial extension that returns self as ID under a certain condition.

extension Identifiable where Self: Hashable, ID == Self {
  public var id: ID {
    return self
  }
}

That way the user can simply provide typealias ID = Self where appropriate to get the default implementation.

Without commenting on the idea itself, I just want to mention that the constraint “Self: Hashable” is redundant. Since ID must be Hashable, you can simply write:

extension Identifiable where ID == Self {
  ...
}
1 Like

But one important note regarding a Identifiable where ID == Self extension: the where constraint will be ignored and the associated type inference will try to infer that ID is always Self. :frowning:

1 Like

If you can demonstrate that it’s a common pattern, sure.

I can’t really think of any.

Edit: in fact, I don’t think it would ever make sense, as mutating any property would then change the ID. That would be like changing an Employee’s department and having their ID change (i.e treating them as an entirely new person).

The whole point of this ID is that it is stable when other properties change.

1 Like

git uses cryptographic hash, which is not the same than a simple hashValue.

As appealing as the flexibility is of the key path approach is, it's really a workaround for a missing language feature: the ability to nominate a differently-named function as the implementation of a protocol requirement.

This feature exists in underscored form as @_implements and has been used within the standard library in a couple of places in order to finalize the ABI. Ideally, this attribute could be productized and the underscore dropped. You would then write something like e.g.:

struct Person: Identifiable {
  @_implements(Identifiable, id)
  var socialSecurityNumber: String
  var name: String
}

Given the potential of this feature, we shouldn't bake into the ABI a workaround that could have negative performance and usability implications for the long term.

31 Likes

Our hashes aren't that simple either ;-) - but imho that's too much off topic.

That isn't the only problem that's solved by the keypath-approach, and the solution in the pitch leads to a lot of indirection as well:
I know it's unfair to bring real-world experience to discussion ;-), but I recently worked in a project which had at least three variants of an id-property - and that was really awkward.
Whenever you needed the id, you saw several options in autocompletion which all looked like a perfect fit, and that is just confusing.
So the name collisions many seem to fear would actually be the better case for me - because with two different names, you would have the decide which one to choose.

I don't think this is a good comparison, and nobody proposed to add such indirection for every protocol.
The thing is that adding a property for Identifiable will lead to a lot of indirection as well - and it will be much more visible:
I expect that the majority of types that will conform already have an id whose name is defined by the context.
When you have a JSON-format which has a customerID, what are you going to do to become Identifiable? Rename your field and diverge from your raw data, or add indirection and two names for the same thing? There's no obvious answer, imho there shouldn't even be the need to think about such questions.

I totally agree. If there is a better feature fit for implementing this, (even if it’s not available, yet) I’m all for it. That seems like a great language feature that would solve any of these KeyPath workaround solutions for other types. Do you think that adding the @implements language feature to Swift would be an easy sell?

I think it's not just an easy sell, it's a mandatory feature in the long term. It falls into a similar bucket to other key missing features such as the ability to disambiguate between different methods of the same name. I'd like to see us go through and mop these up some time soon.

7 Likes

That would be excellent and much better than using a KeyPath.

One question, how does @_implements work when the id attribute already exists but doesn't meet the Identifiable requirements?

ie:

struct Foo: Identifiable {
    // Not suitable for Identifiable
    var id: Bar
    // Would this be possible?
    @_implements(Identifiable, id)
    var uuid: String
}

Bearing in mind this isn't an official feature so the exact behavior of a real implementation would be TBD (and if you use the underscored version you should have no expectations to not be broken in future releases :), but this works:

protocol P {
    associatedtype A
    var foo: A { get }
}

struct S: P {
    var foo: String { "foo" }

    @_implements(P,foo)
    var bar: String { "bar" }
}

func f<T: P>(_ t: T) { print(t.foo) }

f(S()) // prints "bar"

edit: it appears there is a bug in associated type inference (shocking I know) that means if you define S.foo to return a different type, you need to explicitly typealias A = String to ensure it still works.

1 Like

In my post above, I mention that there are some cases where @_implements doesn't work if the conforming type already has a member with the same name as the requirement being satisfied (the expression evaluator treats them as identical and thus ambiguous). Is it reasonable that we could fix that as part of this feature, or would it have other bad implications?

Sounds like a great argument for standardising the name, so you always know where to reach for the ID of something that conforms to Identifiable, instead of tracking down a keypath and working out where it points.

I wouldn't want to be using an API where it seems I'm redundantly referring to customer.customerID and customer.customerName, etc. So I guess I would remap it using CodingKey.

1 Like

Doesn’t a concept already having a private full implementation improve its chances of acceptance?

The final feature needs to be properly designed — the current implementation is just whatever was needed to stabilize the standard library's ABI — but I'd consider that a fairly straightforward bug. In a non-generic context, the concrete implementation that doesn't fulfill the requirement should be called.

3 Likes

I really like this...but to be that person, would it need to remain an @ attribute?

I would argue that returning self from id is questionable design. Do you have a motivating example?

I don't have that much of a strong example, but I use a custom Unique<Key, Value> type to describe sections and items for the new NSDiffableDataSourceSnapshot type. Sections are almost always very simple so there is no need to provide any additional ID type, Key can just be Value, which makes it Unique<Section, Section> in my case.

  public enum Section: Hashable, Identifiable {
    case ...
    case ...
    
    public typealias Identity = Section
  }

Just as a follow-up here from the perspective of Combine. After a number of extensive adoption refactors we came to the conclusion that unfortunately we cant (at this moment) adopt Identifiable. It boils down to the fact that we need the existential form of Subscription and as the current generics implementation we cannot express a constrained existential via generics; i.e. if the type is Identifiable we constrain the identifier type to CombineIdentifier and then still be able to store as an existential. This functionality is needed since the items stored as a Subscription can come from multiple localities even in the same subscriber chain due to things like higher-ordered operators etc. That all being said, as soon as such a feature is possible (plus some other migration features for protocol parenting to allow us to adopt it) we are definitely going to strongly consider the adoption.

3 Likes

Thanks for the update and the effort you have put into exploring this @Philippe_Hausler.