2018-12-31

Swift の Protocol Extension で save(_ models: Sequence<Model>) みたいなことをやる

前提あるいは Array 版

Swift でシンプルな Protocol を用意して...

protocol ModelType: Hashable {}
protocol Repository {
    associatedtype Model: ModelType
    func save(_ models: [Model])
}

これらのプロトコルに適合するモデルとリポジトリーを作ってみる。

// MARK: - モデル  
struct Book: ModelType {
    let title: String
}

// MARK: - リポジトリー
extension Repository {
    func save(_ models: [Model]) {
        models.forEach { model in
            print(model)
        }
    }
}

struct BookRepository: Repository {
    typealias Model = Book
}

今回は Book モデルと BookRepository を作ってみた。Repository Protocol の save メソッドは Protocol Extension で実装した。中身はダミー。ちゃんとやるなら、Dictionary を使ったメモリー・キャッシュか Realm 等を使ったディスク・キャッシュの実装を書くことになると思う。

これらのメソッドの使い方は次の通り:

let array = [ Book(title: "book 1"), Book(title: "book 2"), ]
let repository = BookRepository()
repository.save(array)

Set でも使いたい

さて、時には Array ではなく Set を使いたいケースがある。その場合の使い方は次の通り:

let set: Set = [ Book(title: "book 3"), Book(title: "book 4"), ]
let repository = BookRepository()
repository.save(Array(set))

Repository の save メソッドは Array しか受け付けないので、Set から Array に変換する手間がかかる。

この変換をなくしたい。Array とか Set とか気にせず使いたい。そんなことは可能だろうか?

Set 用のメソッドを用意する

Swift は型を厳密に区別するので、Set 用のメソッドを用意してみる。

protocol Repository {
    associatedtype Model: ModelType
    func save(_ models: [Model])
    func save(_ models: Set<Model>)
}
extension Repository {
    func save(_ models: [Model]) {
        models.forEach { model in
            print(model)
        }
    }
    func save(_ models: Set<Model>) {
        models.forEach { model in
            print(model)
        }
    }
}

こうすると、さっきのコードは Array への変換が不要になる:

let set: Set = [ Book(title: "book 3"), Book(title: "book 4"), ]
let repository = BookRepository()
repository.save(set)

目的達成。ただ、力業で凌いだきらいが強い。もう少しスマートにできないものか?

Sequence を使いたい

Protocol Extension のコードを見てみると、中のコードは全く同じ。forEach を使っている。forEach メソッドは Sequence Protocol にある。そして Sequence Protocol は Array にも Set にも適合されている。

すると、Array や Set のメソッドを用意しなくても、Sequence Protocol に対して save メソッドを用意すれば良さそう。そう、こんな感じに...

protocol Repository {
    associatedtype Model: ModelType
    func save(_ models: Sequence<Model>)
}

残念ながら上のコードは動かない。やりたいことは分かったけど、実現方法が分からない。

というわけで、本記事のタイトルに書いたスタート地点に立った。

Set.union を参考に

そういえば、Set の union メソッドは引数に Set でも Array でも渡すことができた。あの実装はどうなっているんだろう?

コード・ジャンプを使って実装を見てみよう:

extension Set : SetAlgebra where Element : Hashable {
    public func union<S>(_ other: S) -> Set<Set<Element>.Element>
        where Element == S.Element, S : Sequence

なるほど、Sequence.Element で型を指定している。この方法を真似ればよさそう:

protocol Repository {
    associatedtype Model: ModelType
    func save<S>(_ models: S) where S: Sequence, S.Element == Model
}
extension Repository {
    func save<S>(_ models: S) where S: Sequence, S.Element == Model {
        models.forEach { model in
            print(model)
        }
    }
}

これでコードを書いてみると...

let array = [ Book(title: "book 1"), Book(title: "book 2"), ]
let set: Set = [ Book(title: "book 3"), Book(title: "book 4"), ]
let repository = BookRepository()

repository.save(array)
repository.save(set)

Array でも Set でも問題なく使うことができた。

あとがき

Sequence Protocol を意識することで、Array でも Set でも使えるメソッドを用意することができた。やったことはプロトコルへ興味を開げただけ。実装に手詰まっても、Apple のソースコードからヒントを得ることができる。時間があったら Swift の標準プロトコルを見て回るのも面白いかもしれない。