この投稿は「Swift Advent Calendar 2016」 8日目のものです。
今日が火曜日だったら完璧だったのに...
今回は「あなたの知らないCollectionの世界」というテーマでお送りします。
環境
- Xcode8.1
- Swift3.0.1
そもそもCollection (protocol) って
(シーケンスの持つ)要素(element)が複数回、非破壊的に走査することができて、インデックスされた添字によってアクセスできるシーケンス
のことを指すようです。(ちょっと訳が怪しい...
)
普段何気なく使っているArrayもこのCollection(正確には MutableCollection protocolですが) に適合しています。DictionaryやSetなんかも。
Collection protocolの継承図
Collection 自体は、 Indexable と Sequence を継承(に適合) しています。
それ以降はこのCollectionをベースに多岐に渡って様々なprotocol、structに展開されていきます。
最終的にはArrayやDictionary、Set、AnyCollection、StringのCharacterViewなどに辿り着きます。
横に長いので、見たい方は↓をご覧ください。
Collectionに適合していると何ができるのが
Collectionに適合していると、以下のようなことができます
- 「for in」文で列挙ができる
- 定められた添字でアクセスができる (e.g.
array[0]) - 要素の数を調べることができる (count, isEmptyなど)
- filter, map, flatMap, reduce, sortなどの関数が使える
などなど。(※一部、Sequenceのみでもできることがあります)
基本的にはArrayやDictionaryで普段使っていると思うので、馴染みがあるのではと思います。
Collectionを自作の型で使ってみる
試しにCollectionに適合させた自作の型を作ってみます
FizzBuzzをCollectionで
FizzBuzzをCollectionで作ってみます。
limitを与えることで、1〜limit(limit含む)をFizz,Buzzを使って表せるCollectionとして定義してみます。
struct FizzBuzzCollection: Collection {
typealias _Element = String
typealias Index = Int
var startIndex: Index { return 0 }
var endIndex: Index { return limit }
let limit: Int
subscript (position: Index) -> _Element {
precondition((startIndex..<endIndex) ~= position, "Index out of bounds.")
let num = position + 1
switch (num % 3, num % 5) {
case (0, 0): return "Fizz Buzz"
case (0, _): return "Fizz"
case (_, 0): return "Buzz"
default: return "\(num)"
}
}
func index(after i: Index) -> Index {
precondition(i < endIndex, "Can't advance beyond endIndex")
return i + 1
}
}
使ってみる
let fizzbuzzCollection = FizzBuzzCollection(limit: 100)
fizzbuzzCollection.forEach { print($0) }
//1
//2
//Fizz
//4
//Buzz
//Fizz
//...
//98
//Fizz
//Buzz
print(fizzbuzzCollection[4]) // -> "Buzz"
print(fizzbuzzCollection.first as Any) // -> Optional("1")
Collectionに適合するために必要な定義
Collection に適合するためには、少なくとも以下の定義が必要になってきます。
// 要素の始点(Index型)を返す
var startIndex: Self.Index { get }
// 要素の終点(Index型)を返す
var endIndex: Self.Index { get }
// あるindexの次のindex(Index型)を返す
func index(after i: Self.Index) -> Self.Index
// Index型の添字でアクセスを可能にするsubscript
subscript(position: Self.Index) -> Self.Iterator.Element { get }
実はこれらが決まることで、本来は定義が必須となっていますが、以下のものも定まるようになります。
var indices: Self.Indices { get }
subscript(bounds: Range<Self.Index>) -> Self.SubSequence { get }
func makeIterator() -> Self.Iterator
最初、必須だよと書かれてるものが多くて定義大変じゃね...って思っていたのですが、この3つはその上の4つが決まると良いようです ![]()
また、 Index及びElementの型が何であるかtypealiasで定義する必要があります
typealias Element = String
typealias Index = Int
ただ、こちらも以下のようにそれぞれの関数で型を正確に与えてあげることでIndex及びElementの型が定まるので、typealiasでの定義は不要になります。
struct FizzBuzzCollection: Collection {
var startIndex: Int { return 0 }
var endIndex: Int { return limit }
let limit: Int
subscript (position: Int) -> String {
precondition((startIndex..<endIndex) ~= position, "Index out of bounds.")
let num = position + 1
switch (num % 3, num % 5) {
case (0, 0): return "Fizz Buzz"
case (0, _): return "Fizz"
case (_, 0): return "Buzz"
default: return "\(num)"
}
}
func index(after i: Int) -> Int {
precondition((startIndex..<endIndex) ~= position, "Index out of bounds.")
return i + 1
}
}
個人的には、最初にtypealiasで型を書いて...としたほうが宣言時は分かりやすいのですが、使う時に_Elementみたいに見えてしまうので、後者の書き方のほうが好きです ![]()
また、注意点としては、subscriptやindex(after:)では範囲外になってしまうかどうかの判定をしてあげると良いでしょう。
subscript (position: Index) -> _Element {
precondition((startIndex..<endIndex) ~= position, "Index out of bounds.")
...
}
func index(after i: Index) -> Index {
precondition(i < endIndex, "Can't advance beyond endIndex")
...
}
let fizzbuzzCollection = FizzBuzzCollection(limit: 100)
fizzbuzzCollection[99] // -> "Buzz"
fizzbuzzCollection[100] // => Error! "Index out of bounds."
余談
ちなみに、(startIndex..<endIndex) ~= positionの部分を、indices.contains(position)なんてちょっと格好つけてしまうとここに計算コストがかかってしまって遅くなってしまうので注意です。(本人談)
first, lastを取得する
FizzBuzzCollection は Collectionに適合させているので、要素の最初、最後を取得するのは簡単...! ![]()
let fizzbuzzCollection = FizzBuzzCollection(limit: 100)
print(fizzbuzzCollection.first as Any) // => "Optinal("1")"
print(fizzbuzzCollection.last as Any) // ... Error!
あれ、 lastが取れない...だと。
documentを読んでみると、firstはあるけど、lastがない...
lastを使えるようにする
lastを使えるようにするには、 CollectionをBidirectionalCollectionにしてあげる必要があります。
struct FizzBuzzCollection: BidirectionalCollection {
// ...
}
そうすると、
// 指定したindex(i)の前のindexを取得する
func index(before i: Index) -> Self.Index
*index(before:)*を実装しろと言われるので実装します。
struct FizzBuzzCollection: BidirectionalCollection {
func index(before i: Int) -> Int {
precondition(startIndex <= i, "Can't back beyond startIndex")
return i - 1
}
}
これで、lastが晴れて使えるようになります ![]()
let fizzbuzzCollection = FizzBuzzCollection(limit: 100)
print(fizzbuzzCollection.first as Any) => "Optinal("1")"
print(fizzbuzzCollection.last as Any) ... Error!
継承図を見るとこんな感じ。CollectionとBiDirectionalIndexableを継承しています。
ここまで扱えるようになれば自作のCollectionとしては十分なものとなったと思います。
もし、要素へのランダムアクセスに関して適切な計算量で行える実装ができる場合には、 RandomAccessCollection として実装してみてもよいかもしれません。
BidirectionalCollection と比べて必須の実装は増えませんが、
index(_:offsetBy:), distance(from: to:) といったメソッドを計算量 O(1) で実装できるのが望ましいようです。
ちなみにArrayは...
今回はBidirectionalCollection まで話が進みましたが、更に進めていくとArrayにたどり着くことができます。
普段何気なく使っているArrayも、このようにいくつものprotocolを経て成り立っているのがわかります。
そして、改めてprotocol指向なんだなと思い知らされます ![]()
AnyCollectionとは...
AnyCollectionは、Collectionを継承していて、Collectionを型消去して扱うことができるようになるstructです。
おっと... 型消去 がでてきましたね。
型消去に関しては平常心で型を消し去るを見るのをおすすめします。(説明は割愛...)
何に使うのかというと、例えば外からFizzBuzzCollectionという実装を隠したいが、外からはちゃんとこのCollectionを使いたいという場合に、この AnyCollection を使ってあげます。
class MyCollections {
lazy var fizzBuzzCollection: AnyCollection<String> = AnyCollection(FizzBuzzCollection(limit: 100))
// これはNG。
lazy var fizzBuzzCollection: Collection = FizzBuzzCollection(limit: 100)
}
こうすることで、MyCollectionsを外から生成してfizzBuzzCollectionを見るときには、FizzBuzzCollectionという型は見えず、AnyCollection<String>という型で見えるようになり、型を隠蔽することができます。
もう一つの使い方としては、例えばFizzBuzzCollectionと同じく要素がStringのPPAPCollectionがあったとしましょう。
struct PPAPCollection: Collection {
// P-P-A-Pの順で要素をいれる
let elements: (String, String, String, String)
init(_ first: String, _ second: String, _ third: String, _ fourth: String) {
self.elements = (first, second, third, fourth)
}
var startIndex: Int { return 0 }
var endIndex: Int { return 4 }
subscript(index: Int) -> String {
switch index {
case 0: return elements.0
case 1: return elements.1
case 2: return elements.2
case 3: return elements.3
default: fatalError("Index out of bounds.")
}
}
func index(after i: Int) -> Int {
precondition(i < endIndex, "Can't advance beyond endIndex")
return i + 1
}
var ppap: String {
return [elements.0, elements.1, elements.2, elements.3].map {
$0.capitalized
}.joined(separator: "-")
}
}
let ppapCollection = PPAPCollection("pen", "pineapple", "apple", "pen")
print(ppapCollection.ppap)
// => Pen-Pineapple-Apple-Pen!
どちらもfunnyなcollectionということで、これらをまとめる配列を作るとします。
このときに [Collection]としてまとめることができないので、 [AnyCollection<String>] としてAnyCollectionに包んであげることで型消去しつつまとめてあげることができます。
var NG_funnyCollections: [Collection] = [
FizzBuzzCollection(limit: 100),
PPAPCollection("pen", "pineapple", "apple", "pen")
]
// Protocol 'Collection' can only be used as a generic constraint because it has Self or associated type requirements と怒られる
var funnyCollections: [AnyCollection<String>] = [
AnyCollection(FizzBuzzCollection(limit: 100)),
AnyCollection(PPAPCollection("pen", "pineapple", "apple", "pen"))
]
// AnyCollectionとして包んであげることで、どちらも要素が`String`であるCollectionをまとめられた!
ちなみに、AnyCollection<Any> みたいにして何でも...というわけにはいかないので注意です ![]()
内部にlistを所有するようなCollectionを自作してみる
今度は内部にlist(Array)を持つCollectionを自作してみます。
本を表す型Bookと、本棚を表す型BookShelfを用意し、BookShelfにBidirectionalCollectionを適合してみます
struct Book {
let title: String
let content: String
}
struct BookShelf: BidirectionalCollection {
private let list: [Book]
let title: String
init(title: String, list: [Book]) {
self.title = title
self.list = list
}
var startIndex: Int { return self.list.startIndex }
var endIndex: Int { return self.list.endIndex }
subscript (position: Int) -> Book {
return list[position]
}
func index(after i: Int) -> Int {
return self.list.index(after: i)
}
func index(before i: Int) -> Int {
return self.list.index(before: i)
}
}
使ってみる
Collectionを使うメリット
この例だと、使うメリットはなんなのかと言われると、 Array + αをもつ型として定義できるところかと思います。
Collectionを適合させた場合とそうでない場合を比較すると、
let shelf = BookShelf(title: "swiftの本棚", list: ["The Swift Programming Language"])
// 適合させていない場合
shelf.list.forEach { book in prnt(book.title) }
let firstBook: Book? = Shelf.list.first
// 適合させた場合
shelf.forEach { book in prnt(book.title) }
let firstBook: Book? = shelf.first
Shelf.listでもいいじゃんという感じもしますが、その型自身をCollectionと見立てて操作ができるので個人的には良いなと思います。
あとは、以下のような感じで何かしらのモデルをまとめつつ、他の +α をもつときに、Collection化してあげることで、
models.list みたいな冗長さを解消できると思います。
struct SomeViewModels {
private let id: Int
private let models: [SomeViewModel]
}
let models = SomeViewModels(id: 1, models: [SomeViewModel(),...])
models.id
models.list.forEach { model in ... } // models.listってちょっと冗長的
- Collection化すると...
struct SomeViewModels {
private let id: Int
private let models: [SomeViewModel]
}
let models = SomeViewModels(id: 1, models: [SomeViewModel(),...])
models.id
models.forEach { model in ... } // 冗長さを感じなくなった!
とはいいつつも、使い所が難しいかもしれないです。が、やはり内部に持つlist(配列)を外から見えなくしつつ、その型をCollectionとして扱えるのは良いのかもしれません。
その他Documentを見て気になったもの
どうやら、Swift4で IndexableBase, Indexable, BidirectionalIndexable, RandomAccessIndexableなどが削除されるようなので、もし使ってる場合は今のうちに代案がないか考えておきましょう...!
Deprecated: it will be removed in Swift 4.0. Please use 'Collection' instead.
一応、 InedexableBase, Indexableの場合はCollectionを使えと書いてあります ![]()
まとめ
普段使っているArrayやDictionaryの先祖を辿ってみる、実際にprotocolを適合させた自作の型を使ってみると理解が深まりますね!
本当はSubSequenceとかIteratorとかまで深掘りしたかったのですが、迷宮入りしそうだったので今回は諦めて、なるべくそれを今回は意識しなくて済むようにさせて頂きました ![]()
今回自分でまとめるまで、AnyCollectionってなんぞや?って感じだったので、勉強にもなりました。
また、自作の型にCollectionを適合させる場合には、範囲外アクセスしてしまわぬよう気をつけましょう。 ![]()
何かあれば気軽に編集リクエストとかでもお投げください。
参考
おまけ
Collectionとはちょっと離れますが、Iteratorだけを使ってFizzBuzzもできるよということで貼っておきます。
let makeFizzBuzzIterator: (Int) -> AnyIterator<String> = { limit in
var index = 0
return AnyIterator<String> {
defer { index += 1 }
if index < limit {
let num = index + 1
switch (num % 3, num % 5) {
case (0, 0): return "Fizz Buzz"
case (0, _): return "Fizz"
case (_, 0): return "Buzz"
default: return "\(num)"
}
} else {
return nil
}
}
}
makeFizzBuzzIterator(100).forEach { print($0) }