Help us understand the problem. What is going on with this article?

あなたの知らないCollectionの世界

More than 3 years have passed since last update.

この投稿は「Swift Advent Calendar 2016」 8日目のものです。

今日が火曜日だったら完璧だったのに...
今回は「あなたの知らないCollectionの世界」というテーマでお送りします。

環境
  • Xcode8.1
  • Swift3.0.1

そもそもCollection (protocol) って

(シーケンスの持つ)要素(element)が複数回、非破壊的に走査することができて、インデックスされた添字によってアクセスできるシーケンス

のことを指すようです。(ちょっと訳が怪しい... :pray: )
普段何気なく使っているArrayもこのCollection(正確には MutableCollection protocolですが) に適合しています。DictionarySetなんかも。

Collection protocolの継承図

スクリーンショット 2016-12-03 11.13.08.png

Collection 自体は、 IndexableSequence を継承(に適合) しています。
それ以降はこのCollectionをベースに多岐に渡って様々なprotocolstructに展開されていきます。

最終的にはArrayDictionarySetAnyCollection、StringのCharacterViewなどに辿り着きます。
横に長いので、見たい方は↓をご覧ください。

Collectionに適合していると何ができるのが

Collectionに適合していると、以下のようなことができます

  • 「for in」文で列挙ができる
  • 定められた添字でアクセスができる (e.g. array[0])
  • 要素の数を調べることができる (count, isEmptyなど)
  • filter, map, flatMap, reduce, sortなどの関数が使える

などなど。(※一部、Sequenceのみでもできることがあります)
基本的にはArrayDictionaryで普段使っていると思うので、馴染みがあるのではと思います。

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つが決まると良いようです :thinking:

また、 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みたいに見えてしまうので、後者の書き方のほうが好きです :v_tone1:

また、注意点としては、subscriptindex(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に適合させているので、要素の最初、最後を取得するのは簡単...! :star2:

let fizzbuzzCollection = FizzBuzzCollection(limit: 100)
print(fizzbuzzCollection.first as Any) // => "Optinal("1")"
print(fizzbuzzCollection.last as Any) // ... Error!

あれ、 lastが取れない...だと。
documentを読んでみると、firstはあるけど、lastがない...

lastを使えるようにする

lastを使えるようにするには、 CollectionBidirectionalCollectionにしてあげる必要があります。

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が晴れて使えるようになります :clap_tone1:

let fizzbuzzCollection = FizzBuzzCollection(limit: 100)
print(fizzbuzzCollection.first as Any) => "Optinal("1")"
print(fizzbuzzCollection.last as Any) ... Error!

継承図を見るとこんな感じ。CollectionBiDirectionalIndexableを継承しています。

スクリーンショット 2016-12-03 18.07.20.png

ここまで扱えるようになれば自作のCollectionとしては十分なものとなったと思います。
もし、要素へのランダムアクセスに関して適切な計算量で行える実装ができる場合には、 RandomAccessCollection として実装してみてもよいかもしれません。
BidirectionalCollection と比べて必須の実装は増えませんが、
index(_:offsetBy:), distance(from: to:) といったメソッドを計算量 O(1) で実装できるのが望ましいようです。

ちなみにArrayは...

今回はBidirectionalCollection まで話が進みましたが、更に進めていくとArrayにたどり着くことができます。

スクリーンショット 2016-12-03 18.19.31.png

普段何気なく使っているArrayも、このようにいくつものprotocolを経て成り立っているのがわかります。
そして、改めてprotocol指向なんだなと思い知らされます :laughing:

AnyCollectionとは... :thinking:

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>という型で見えるようになり、型を隠蔽することができます。

スクリーンショット 2016-12-04 1.38.50.png

もう一つの使い方としては、例えばFizzBuzzCollectionと同じく要素がStringPPAPCollectionがあったとしましょう。

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> みたいにして何でも...というわけにはいかないので注意です :pray:

内部にlistを所有するようなCollectionを自作してみる

今度は内部にlist(Array)を持つCollectionを自作してみます。
本を表す型Bookと、本棚を表す型BookShelfを用意し、BookShelfBidirectionalCollectionを適合してみます

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などが削除されるようなので、もし使ってる場合は今のうちに代案がないか考えておきましょう...!

http://swiftdoc.org/v3.0/protocol/IndexableBase/

Deprecated: it will be removed in Swift 4.0. Please use 'Collection' instead.

一応、 InedexableBase, Indexableの場合はCollectionを使えと書いてあります :star2:

まとめ

普段使っているArrayやDictionaryの先祖を辿ってみる、実際にprotocolを適合させた自作の型を使ってみると理解が深まりますね!
本当はSubSequenceとかIteratorとかまで深掘りしたかったのですが、迷宮入りしそうだったので今回は諦めて、なるべくそれを今回は意識しなくて済むようにさせて頂きました :pray:
今回自分でまとめるまで、AnyCollectionってなんぞや?って感じだったので、勉強にもなりました。
また、自作の型にCollectionを適合させる場合には、範囲外アクセスしてしまわぬよう気をつけましょう。 :dizzy_face:

何かあれば気軽に編集リクエストとかでもお投げください。

参考

おまけ

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) }
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした