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 の標準プロトコルを見て回るのも面白いかもしれない。

1 comment:

  1. Công ty dịch thuật miền trung - MIDtrans có Giấy phép kinh doanh số 3101023866 cấp ngày 9/12/2016 là đơn vị chuyên cung cấp dịch vụ dịch thuật, phiên dịch dành các cá nhân, hệ thống thương hiệu Dịch thuật sài gòn 247 Dịch thuật chuyên nghiệp MIDtrans Vietnamese translation dịch thuật, phiên dịch sài gòn Dịch thuật Đà Nẵng Pro . Dịch thuật MIDtrans tự hào với đội ngũ lãnh đạo với niềm đam mê, khát khao vươn tầm cao trong lĩnh vực dịch thuật, đội ngũ nhân sự cống hiến và luôn sẵn sàng cháy hết mình. Chúng tôi phục vụ từ sự tậm tâm và cố gắng từ trái tim những người dịch giả. Hotline: 0947688883. Rất hân hạnh được phục vụ quý khách

    ReplyDelete