読者です 読者をやめる 読者になる 読者になる

SequenceTypeの利便性 in Swift 2.2

iOS Swift

f:id:vasilyjp:20160324105731j:plain

Making Sequences work for you

こんにちは!

VASILYのiOSエンジニアにこらすです。 SwiftのコントリビューターとSwift EvolutionSE-0053の作者です。 他のOSSプロジェクトにも貢献してるので興味があればGithubでフォローしてください。

今回のトピックはSwift2.0以降のSequenceTypeというプロトコルと、その内部的な動きについて紹介します。classstructSequenceTypeプロトコルに準拠させると、for inループやmap, filterなどを使えるようになります。

さあ、始めましょう!

struct Unique<T: Comparable> {
    private var backingStore: [T] = []

    var count: Int {
        return backingStore.count
    }

    func setHas(element: T) -> Bool {
        return backingStore.filter { $0 == element }.count != 0
    }

    mutating func append(element: T) {
        guard !setHas(element) else { return }

        backingStore.append(element)
    }
}

このUniqueという構造体はとてもシンプルです。Comparableに準拠した型の要素をappendすると内部的な配列に要素を追加します。配列中に同じ要素が存在する場合は追加しません。

それではUniqueをテストしてみましょう!

var mySet = Unique<String>()

// Our set can
mySet.setHas("A Careless Cat") //false
mySet.append("A Careless Cat")
mySet.setHas("A Careless Cat") //true

mySet.append("A Careless Cat") //すでにある文字列
mySet.count //まだ1です!

//もうちょっと項目追加しましょう!
mySet.append("A Dangerous Dog")
mySet.append("A Diamond Dog")
mySet.append("Petty Parrot")
mySet.append("North American Reckless Raccoon")
mySet.append("A Monadic Mole")

動物は十分入ったので、名前をprintしてみよう!

for animal in mySet {
    println(animal)
}

f:id:vasilyjp:20160324103132p:plain あれ?うまくいきませんね。。。:

なぜうまくいかなかったかというと、for inを使うために必要な実装が足りないからです。 Stringの配列でfor inを書くのは、下記の書き方と同じです。

let vowels = ["A","E","I","O","U"]

var gen = vowels.generate()
while let letter = gen.next() {
    print(letter)
}

generate()SequenceTypeプロトコルのメソッドです。 UniqueSequenceTypeプロトコルに準拠してないのでfor inが動かないのです。 それでは、SequenceTypeプロトコルに必要な条件を見て実装してみましょう!

public protocol SequenceType {
    associatedtype Generator : GeneratorType
    public func generate() -> Self.Generator
}

generate()の戻り値の型は型推論が効くので、typealias Generator = ...を書く必要はありません。

UniqueSequenceTypeプロトコルに準拠させて、generate()メソッドを実装するべきですが、まだGeneratorTypeのことをよく知りません。 それではGeneratorTypeをチェックしましょう!

Generator とは?

public protocol GeneratorType {
    associatedtype Element
    public mutating func next() -> Self.Element?
}

SequenceTypeと同じように一つのメソッドしかありません。next()の戻り値の型は、Element型となっていますが型推論が効きます。[String]であれば、実際にはStringになります。 Generatornext()メソッドで、保持しているデータを順番に返します。最後のデータを返したあと再度next()を呼ぶとnilを返します。

GeneratorTypeを使い終わると再利用できません。同じデータセットをもう一度GeneratorTypeで読みたければ、新しくGeneratorTypeを生成する必要があります。

そして、generate()を実装する時に戻り値のGeneratorTypeと他のGeneratorType変数の状態を共有しないようにしなければいけません。

基本的な上限があるGeneratorType:

struct CountToGenerator: GeneratorType {

    private var limit: Int
    private var currentCount = 0

    init(limit: Int) {
        self.limit = limit
    }
    mutating func next() -> Int? {
        guard currentCount < limit else { return nil }

        defer { currentCount += 1 }
        return currentCount
    }
}

var goTillTen = CountToGenerator(limit: 10)
while let num = goTillTen.next() {
    print(num)
}

SequenceTypeGeneratorTypeを学んだので、Unique専用のGeneratorTypeを作りましょう!

class UniqueGenerator<T>: GeneratorType {
    private var _generationMethod: () -> T?
    init(_generationMethod: () -> T?) {
        self._generationMethod = _generationMethod
    }

    func next() -> T? {
        return _generationMethod()
    }
}

extension Unique: SequenceType {
    func generate() -> UniqueGenerator<T> {
        var iteration = 0

        return UniqueGenerator {
            if iteration < self.backingStore.count {
                let result = self.backingStore[iteration]
                iteration += 1
                return result
            }

            return nil
        }
    }
}

ここまで学んだことで、UniqueGeneratorTypeの実装ができましたが、もう少し短く書けます。 AnyGeneratorというGenericタイプを使うとUniqueGeneratorを作る必要がありません。

extension Unique: SequenceType {
    func generate() -> AnyGenerator<T> {
        var iteration = 0

        return AnyGenerator {
            if iteration < self.backingStore.count {
                let result = self.backingStore[iteration]
                iteration += 1
                return result
            }

            return nil
        }
    }
}

もう一度for inループを書きましょう!

for item in mySet {
    print(item)
}

やっと動きました!

mapfilterも動きます!やった!

let cnt = mySet.map { Int($0.characters.count) } //[14, 15, 13, 12, 31, 14]
mySet.filter { $0.characters.first != "A" } //["Petty Parrot", "North American Reckless Raccoon"]

Controlling Sequences with Sequences

SequenceTypeの実装次第で、とても大きいリストや無限リストを作ることができます。 そういったリストから先頭からn個取り出そうとすると、そのまま使うと無限ループになってしまいます。

例えば下記のThePatternは無限リストです。 for inで使うと、Generatornilを返さないため、永遠に01を返します。

class ThePattern: SequenceType {
    func generate() -> AnyGenerator<Int> {
        var isOne = true
        return AnyGenerator {
            isOne = !isOne
            return isOne ? 1 : 0
        }
    }
}

// 無限ループ
for i in ThePattern() {
    print(i)
}

この無限リストから

class First<S: SequenceType>: SequenceType {

    private let limit: Int
    private var counter: Int = 0
    private var generator: S.Generator

    init(_ limit: Int, sequence: S) {
        self.limit = limit
        self.generator = sequence.generate()
    }

    func generate() -> AnyGenerator<S.Generator.Element> {
        return AnyGenerator {
            defer { self.counter += 1 }
            guard self.counter < self.limit else { return nil }
            return self.generator.next()
        }
    }
}

for item in First(5, sequence: ThePattern()) {
    print(item) // 0 1 0 1 0
}

いいですね! First(n, sequence: s) を呼び出すと、sの先頭n個の要素を取り出せます。 sが無限リストだとしても最初のn個しかチェックしないので効率的です。

まとめ

配列のように扱うクラスやコンテナクラスであればSequenceTypeプロトコルを採用しましょう。 コードが読みやすくなりますし、自然にfor inmap, filterを使えるようになります。 ぜひ気軽にSequenceTypeのプロトコルを使ってみてください。

最後に

VASILYではSwift好きなiOSエンジニアを募集しています!興味があればぜひ応募してみてください。 www.wantedly.com