Swiftで試すデザインパターン - ArrayやDictionaryを支えるIteratorパターン


教科書:Java言語で学ぶデザインパターン入門

「数学ガール」で有名な結城浩先生の「Java言語で学ぶデザインパターン入門1」を購入しました2
せっかくの機会なので、普段使っているSwiftで実装しながら、勉強ノートを作っていこうと思います。
本記事は「第1章 Iterator 1つ1つ数え上げる」を参考に作成しました。

Iteratorパターンの嬉しさがなんとなくわかってきた!

はじめてIteratorパターンに触れたとき(JavaScriptのIteratorでした)は、何が嬉しいのかまったくわかりませんでした。
今回、自分で手を動かして具体的に実装したり、SwiftのSequencemapのソースコードを眺めているうちにIteratorパターンの嬉しさがなんとなく実感できた気がします。
同じような気持ちを抱いている方の参考になると嬉しいです!

Iteratorパターンとは?

ArrayやDictionaryのように、何かの集まりの全体を順番にスキャンしていくときに使われるデザインパターンです。
少し長い引用になりますが、「Java言語で学ぶデザインパターン入門1」では以下のように説明されています。

for文のi++でiを1ずつ増加させていくと……配列arrの要素全体を最初から順番にスキャン(走査)していることになります。ここで使われている変数iの働きを抽象化し、一般化したものを、デザインパターンでは、
  Iteratorパターン
と呼んでいます。
Iteratorパターンとは、何かがたくさん集まっているときに、それを順番に指し示していき、全体をスキャンしていく処理を行うためのものです。iterate(イテレート)という英単語は何かを「繰り返す」という意味です。3

Iteratorパターンの登場人物

Iteratorインターフェース

要素のスキャンを実行する型が持つインターフェースです。
いろいろな流派があるそうですが、シンプルなものとして、以下のようなprotocolが考えられます。

protocol MyIteratorProtocol {
    // 配列などが保持する要素の型
    associatedtype Element
    // 次の要素があるかどうかを返すメソッド
    func hasNext() -> Bool
    // スキャン中に次の要素を返すメソッド
    mutating func next() -> Element?
}

Aggregate(集合)インターフェース

配列やDictionaryのように「何か」の集まりを保持する型が持つインターフェースで、iteratorを生成するメソッドを持ちます。

protocol AggregateProtocol {
    // 配列などが保持する要素の型
    associatedtype Element
    // iteratorメソッドが返すIteratorの型、Elementが一致する必要がある
    associatedtype Iterator: MyIteratorProtocol where Iterator.Element == Element
    // iteratorを生成するメソッド
    func iterator() -> Iterator
}

ConcreteIterator / ConcreteAggregate

IteratorインターフェースとAggregateインターフェースを実装(準拠)した具体的な型たちです。

具体例:Iteratorパターンを実装してみる

ここでは例として、Bird型の集合を保持するBirdCage型とそのIteratorであるBirdCageIteratorを実装していきます。

Bird

名前を保持するだけのstructです。

Bird.swift
struct Bird {
    let name: String
}

BirdCage

Birdの集合を保持するstructを定義します。
AggregateProtocolに準拠するために、associatedTypeのaliasを設定して、iteratorメソッドを実装します。
ここでは簡単のためにBirdの集合自体は配列で保持します。

BirdCage.swift
struct BirdCage: AggregateProtocol {
    // associatedTypeに具体的な型を設定する
    typealias Element = Bird
    typealias Iterator = BirdCageIterator

    // iteratorを返すメソッドを実装する
    func iterator() -> Iterator {
        BirdCageIterator(birds: birds)
    }

    // ここでは簡単のためにArrayとして集合を保持する
    private var birds: [Bird] = []

    // birdを鳥籠に入れるメソッド
    mutating func put(bird: Bird) {
        birds.append(bird)
    }

    // indexを指定してbirdの様子を覗く
    func peek(birdOf index: Int) -> Bird? {
        index < birds.count ? birds[index] : nil
    }
}

BirdCateIterator

BirdCateのIteratorを定義します。
MyIteratorProtocolに準拠するために、associatedTypeのaliasを設定して、hasNext/nextメソッドを実装します。

BirdCateIterator.swift
struct BirdCageIterator: MyIteratorProtocol {
    // associatedTypeに具体的な型を設定する
    typealias Element = Bird

    // スキャンするための集合を配列で保持する
    private let birds: [Bird]
    private var index: Int = 0

    init(birds: [Bird]) {
        self.birds = birds
    }

    func hasNext() -> Bool {
        index < birds.count
    }

    // メソッドが呼ばれたら、自身のindexをひとつ進める
    mutating func next() -> Bird? {
        let bird = index < birds.count ? birds[index] : nil
        index += 1
        return bird
    }
}

使ってみる

集合の各要素をprintする関数を定義して呼び出します。
ポイントは、Aggregate: AggregateProtocolのように型パラメータを使って関数を定義することです。
このように実装された関数は、Iteratorパターンで実装されている型(AggregateProtocolに準拠している型)すべてに対して使うことができます。

func printElements<Aggregate: AggregateProtocol>(of aggregate: Aggregate) {
    var iterator = aggregate.iterator()
    while iterator.hasNext() {
        let element = iterator.next()
        print(String(describing: element!))
    }
}

var cage = BirdCage()
cage.put(bird: Bird(name: "Budgerigar"))
cage.put(bird: Bird(name: "Cockatiel"))
cage.put(bird: Bird(name: "Parakeet"))
cage.put(bird: Bird(name: "Parrot"))

printElements(of: cage)

// Bird(name: "Budgerigar")
// Bird(name: "Cockatiel")
// Bird(name: "Parakeet")
// Bird(name: "Parrot")

Iteratorパターンの何が嬉しいのか?

具体的な実装(の詳細)とは関係なく使用できる

先ほどのprintElements関数は、具体的なBirdCage型に対してではなく、「AggregateProtocolに準拠したAggregate一般」に対して実装されています。
そのため、具体的なBirdCageBirdCageIteratorが、iteratornextメソッドをどのように実装しているかとは全く関係なく、printElementsが引数aggregate: Aggregateを使うことができます。

printElementsを便利に使う

この利点を享受しつつ、printElementsをより便利に使うためには、printElementsAggregateProtocolのextensionに定義してやります。

AggregateProtocol.swift
extension AggregateProtocol {
    func printEelements() {
        var iterator = self.iterator()
        while iterator.hasNext() {
            let element = iterator.next()
            print(String(describing: element!))
        }
    }
}

var cage = BirdCage()
cage.put(bird: Bird(name: "Budgerigar"))
cage.put(bird: Bird(name: "Cockatiel"))

// AggregateProtocolに準拠しているすべてのstructからprintElementsを呼ぶことができる!
cage.printElements()

ArrayやDictionary, StringでもIteratorパターンが使われている

Swift標準のIteratorProtocol

実はSwiftには標準でIteratorProtocolというものが用意されています4

public protocol IteratorProtocol {
    associatedtype Element
    mutating func next() -> Element?
}

Iteratorパターンで実装された Sequence protocol

そして、ArrayやDictionary, Stringなどの様々なstructが準拠しているSequenceプロトコルがIteratorProtocolを内部で利用しています5

public protocol Sequence {
    associatedtype Element
    associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
    __consuming func makeIterator() -> Iterator
    ...
}

この部分の定義は、本記事で作成したAggregateProtocolそのものです。
まさにこのSequenceというプロトコルがIteratorパターンを使って実装されているのです。

mapの実装にもIteratorが使われている

たとえば、Arrayではお馴染みのmapメソッドは、実はSequenceプロトコルのextensionに実装されています。
そのため、ArrayやRangeはもちろん、DictionaryやStringについてもmapメソッドを呼び出すことができます。

[1, 2, 3].map { $0 + 1 }
(0...2).map { $0 * 2 }
["key1": 1, "key2": 2].map { (k, i) in i }
"Hello, world!".map { $0.uppercased() }

そして、mapメソッドの内部ではiteratorが使われています6
ArrayやDictionary, Range, Stringの内部実装やそれぞれに定義されたIteratorの実装はそれぞれに異なるはずですが、IteratorProtocolのインターフェースを通じてmapメソッドをひとつの実装で済ますことができるわけです。

extension Sequence {
    ...
    @inlinable
    public func map<T>(
      _ transform: (Element) throws -> T
    ) rethrows -> [T] {
        let initialCapacity = underestimatedCount
        var result = ContiguousArray<T>()
        result.reserveCapacity(initialCapacity)

        var iterator = self.makeIterator()

        // Add elements up to the initial capacity without checking for regrowth.
        for _ in 0..<initialCapacity {
            result.append(try transform(iterator.next()!))
        }
        // Add remaining elements, if any.
        while let element = iterator.next() {
            result.append(try transform(element))
        }
        return Array(result)
    }
}

おわりに

Iteratorパターンの嬉しさが実感できたでしょうか?
次回のパターンもお楽しみに!

環境

  • Xcode 11.6
  • Swift 5.2.4

参考