[Swift] データが持つIdentifierに型を与えてタイプセーフにする

データは多くの場合、Identifierを持っており、そのIdentifierはデータを取得することに使用されます。

では、データが持つIdentifierはSwiftではどのように扱うべきでしょうか?

簡単な例を用いてSwiftが持つ型システムで遊んでみたいと思います。

struct Book {
let identifier: String
...
}

自身を表すIdentifierをString型として保持するBookというデータを用意しました。

そして、Identifierを元にBookを取得するメソッドも用意します。

func fetchBook(by identifier: String) -> Book

これでBookのIdentifierさえわかればBookをしゅとくできるようになりました。

次に、データの種類を増やしてみます。

struct Book {
let identifier: String
...
}
struct Music {
let identifier: String
...
}

Musicというデータです。Bookと同じようにString型のIdentifierを持っています

Musicの取得メソッドも用意します。

func fetchBook(by identifier: String) -> Book
func fetchMusic(by identifier: String) -> Music

ここで少し気になることが出てきます。

それは、fetchBookメソッドにMusicのIdentifierが渡される可能性があることです。

BookからIdentifierを取り出し、色々受け渡しをしているうちに間違ってMusicのIdentifierとして扱われてしまうかもしれません。

let book: Book
let identifier: String = book.identifier
// 色々な処理が行われ...
fetchMusic(by: identifier) // 間違ってBookのIdentifierでMusicを取得しようとしてしまった

と、ちょっと雑な例ですが、実際に記述は可能ですしコンパイルも通せます。

型に強いSwiftであれば、このような問題はできればコンパイル時にわかるようにしたいものです。

データごとにIdentifierの型をつくる

さっきような問題が起きる原因はBookもMusicもIdentifierが同じString型であることです。
取り出してしまえば、ただの文字列にすぎません。

それぞれのデータからIdentifierだけを取り出しても、どのデータを表すIdentifierなのかを型から知ることができればコンパイル時に守るコードを書くことが可能になります。

では、最初のアプローチとしてデータごとにIdentifierの型を用意してみます。

struct Book {
struct Identifier {
let rawValue: String
}
  let identifier: Identifier
...
}
struct Music {
struct Identifier {
let rawValue: String
}
  let identifier: Identifier
...
}

それぞれにInner-structでIdentifier型を用意し、その中に実際のString型のIdentifierを保持するようにします。

func fetchBook(by identifier: Book.Identifier) -> Book
func fetchMusic(by identifier: Music.Identifier) -> Music

取得するメソッドの引数もデータごとのIdentifier型に変更します。

let book: Book
let identifier: Book.Identifier = book.identifier
// 色々な処理が行われ...
fetchMusic(by: identifier) // ❌コンパイルエラー

すると、先ほどのミスが発生してしまうコードがコンパイルによって検出できるようになります。

もう少しラクにIdentifier型を用意する

先ほどの例でタイプセーフにIdentifierを扱えるようになりましたが、データの定義は少し面倒になりました。

設計はこのままで記述をもう少しスマートにできないかを考えてみます。

Identifier型をGeneric-typeにする

struct Identifier<TargetType, RawValueType : Hashable> : Hashable {
  static func ==(lhs: Identifier<TargetType, RawValueType>, rhs: Identifier<TargetType, RawValueType>) -> Bool {
return lhs.rawValue == rhs.rawValue
}
  var hashValue: Int {
return rawValue.hashValue
}
  let rawValue: RawValueType
  init(_ rawValue: RawValueType) {
self.rawValue = rawValue
}
}
struct Book {
let identifier: Identifier<Book, String>
}
struct Music {
let identifier: Identifier<Music, String>
}

データごとに用意していたIdentifier型をGeneric-typeとして定義しました。

Identifier型はデータの型と、そのデータのIdentifierの型を持ちます。

これでデータ定義のたびに専用のIdentifier型を定義する必要がなくなりました。

しかし、次のような打ち間違いが発生しそうです。

struct Music {
let identifier: Identifier<Book, String> // MusicなのにBook 😲
}

こういうヒューマンエラーもなんとかしたいですね。

データにも工夫を加えてみます。

データの型にIdentifierを持つことを示すProtocolを導入する

次のようなProtocolを用意し、BookとMusicに適用します。

protocol IdentifiableType {
associatedtype IdentifierRawValueType : Hashable
var identifier: Identifier<Self, IdentifierRawValueType> { get }
}

データ型に適用します。

struct Book : IdentifiableType {
let identifier: Identifier<Book, String>
}
struct Music : IdentifiableType {
let identifier: Identifier<Music, String>
}

こんな感じになります。

すると先ほどのタイプミスはコンパイルエラーにできます。

struct Music : IdentifiableType {
let identifier: Identifier<Book, String>
// ❌Bookの部分はIdentifiableType.Selfでなければならないのでコンパイルエラー
}

ついでにこんな風にするともっと記述減らせます。

protocol IdentifiableType {
associatedtype IdentifierRawValueType : Hashable
var identifier: Identifier<Self, IdentifierRawValueType> { get }
typealias ID<T: Hashable> = Identifier<Self, T>
}
struct Book : IdentifiableType {
var identifier: ID<String>
}

まとめ

このような考え方はSwiftのKeyPathやRealmのThreadSafeReferenceなどでも見ることができます。

プリミティブ型で表されるものも別の型で包むことでタイプセーフにすることができます。

以上、Swiftの型遊びでした 🤠

追記 (2018/4/17)

この記事で紹介した実装方法とかなり近いライブラリが登場していました!