Identifiable Protocol
01 何が問題だったのか
エンティティの同一性を値の等価性とは別に扱いたい場面は、多くのコードで発生します。たとえば連絡先を表す次のような型を考えます。
struct Contact {
var id: Int
var name: String
}
let john = Contact(id: 1000, name: "John Appleseed")
var johnny = john
johnny.name = "Johnny Appleseed"
ここで john と johnny は名前が異なるため値としては等しくありませんが、「同じ人物」を指しているという点では同一です。逆に、偶然別人と名前や他の属性が一致したとしても、それは「別人」です。このように、エンティティの状態(state)と同一性(identity)を区別して扱える仕組みが、ジェネリックなコードで広く必要とされます。
class の同一性では足りないケース
class のインスタンスであれば ObjectIdentifier や === で同一性を判定できます。しかし次のようなケースには対応できません。
- 値型(struct / enum)のスナップショット間で同一性を比較したい
- 永続化したデータや、プロセスをまたぐデータなど、インスタンスのメモリアドレスが共有できない状況で同一性を判定したい
また、値型に対して同一性を与えるためだけに class を導入するのは、割り当てコストの面でも不経済です。
ユーザーインターフェースにおける差分更新
代表的なユースケースのひとつがユーザーインターフェースの差分更新です。たとえば次のようなお気に入り連絡先リストを考えます。
struct FavoriteContactList: View {
var favorites: [Contact]
var body: some View {
List(favorites) { contact in
FavoriteCell(contact)
}
}
}
リストの中身が更新されたとき、良質な UX を提供するためには「エンティティの同一性」と「そのエンティティの状態の表現」を区別する必要があります。同一エンティティの state だけが変化した場合、古い要素を削除して新しい要素を挿入するのではなく、その場で更新するのが望ましい挙動です。さらに要素が移動しながら state も変わるようなケースも、差分アルゴリズムで正しく扱いたくなります。
こうした差分計算は UI 層だけでなくモデル層で行いたいこともあります。バックグラウンドで差分を計算してからメインスレッドで UI に反映したい、あるいは同じデータを複数のビューで表示しているため UI 層ごとに計算するのは無駄、といった状況です。モデル層のコードは UI フレームワークに依存したくないため、UI フレームワークが独自に定義した同一性プロトコルには頼れません。
各ライブラリが独自定義してしまう問題
標準ライブラリに同一性を表すプロトコルがないと、各ライブラリが似たようなプロトコルを独自に定義することになります。SwiftUI のように主要フレームワークが独自の Identifiable を持つと、他のライブラリは同じ概念なのに互いに互換性のない型を押し付けられることになり、結局どのライブラリもさらに自前の定義を用意せざるを得ません。共通概念として標準ライブラリに置くべき、というのがこの提案の動機です。
02 どのように解決されるのか
標準ライブラリに、エンティティの同一性を表す Identifiable プロトコルを追加します。
/// A class of types whose instances hold the value of an entity with stable identity.
protocol Identifiable {
/// A type representing the stable identity of the entity associated with `self`.
associatedtype ID: Hashable
/// The stable identity of the entity associated with `self`.
var id: ID { get }
}
Identifiable に適合する型は、自身が表すエンティティの安定した同一性を id プロパティとして公開します。差分アルゴリズムやユーザーインターフェースライブラリ、その他のジェネリックなコードは、このプロトコルを通じてスナップショット間の同一性を判定できます。
値型での使い方
適合は単純で、id プロパティを持たせるだけです。
struct Contact: Identifiable {
var id: Int
var name: String
}
ID は associatedtype なので、Int / String / UUID など用途に応じた任意の Hashable な型を選べます。永続化や分散環境での同一性、性能要件、利便性など、どの表現が適切かはエンティティごとに異なるためです。
プロパティ名を id としているのは、この語が識別子として広く使われている term of art であり、短い名前で自然に適合を書けるようにするためです。
class に対するデフォルト実装
class については、ObjectIdentifier を使ったデフォルト実装が用意されます。
extension Identifiable where Self: AnyObject {
var id: ObjectIdentifier {
return ObjectIdentifier(self)
}
}
このため、インスタンスの同一性をそのままエンティティの同一性として扱いたい class は、id を実装しなくても Identifiable に適合できます。
final class Contact: Identifiable {
var name: String
init(name: String) {
self.name = name
}
}
必要であれば、class でも独自の id を明示して上書きできます。
final class Contact: Identifiable {
let id: Int
let name: String
init(id: Int, name: String) {
self.id = id
self.name = name
}
}
具象型を適合させない設計
UUID / Int / String のような型は「識別子として使われる」ことはあっても、それ自身が同一性を持つエンティティではありません。そのためこれらの具象型には Identifiable 適合は追加されません。Identifiable はあくまでエンティティを表す型のためのプロトコルです。
Future Directions
提案ではスコープを絞るために見送られた拡張方向もいくつか挙げられています。いずれも speculative な方向性で、実現を約束するものではありません。
Identifiableベースの collection diffing: 現在BidirectionalCollectionにはEquatableな要素向けのdifference(from:)がありますが、将来的にIdentifiableな要素向けの同等の便利メソッドを追加し、両方に適合する型ではIdentifiableの方を優先する、という案が検討されています。Optionalの条件付き適合:Optional: Identifiable where Wrapped: Identifiableのような条件付き適合も、将来的に追加される余地があります。