# Sequence プロトコル

Sequenceプロトコルとは、Swiftの標準ライブラリで提供されているプロトコルの一種です。Sequenceプロトコルに準拠する型を実装することによって、for-in文で順番に要素にアクセスできるようになります。Array型やDictionary型もSequenceプロトコルに準拠しているため、for-in文で繰り返し処理をすることができます。

SwiftのSequenceプロトコルは、次のようなインターフェースを提供しています。

# forEach

forEach は、全ての要素に順番にアクセスすることができます。

let a = [1,2,3,4,5,6,7,8,9,10]

a.forEach({ element in 
  print(a)
})

// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
// 10

# filter

filter は、指定した条件を満たした要素のみを返すことができます。引数にクロージャを渡して、戻り値にBool型を指定します。

let a = [1,2,3,4,5,6,7,8,9,10]

// 偶数だけをフィルターする
let filtered = a.filter({ element in 
  return element % 2 == 0
})

print(filtered) // [2, 4, 6, 8, 10]

TIP

暗黙的なreturn簡略引数名トイリングクロージャ を使ってシンプルに書くことができます。

let a = [1,2,3,4,5,6,7,8,9,10]

let filtered = a.filter { $0 % 2 == 0 }

print(filtered) // [2, 4, 6, 8, 10]

# map

map は、全ての要素を順番にアクセスし、クロージャから返された値を元に新しいシーケンスを生成します。

let a = [1,2,3,4,5,6,7,8,9,10]

// 各要素を2倍にする
let mapped = a.map({ element in 
  return element * 2
})

print(mapped) // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

mapを使うことによって、型の変換もすることができます。

let a = [1,2,3,4,5,6,7,8,9,10]

// 各要素を2倍にする
let mapped = a.map({ element in 
  return String(element)
})

print(mapped) // ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]

TIP

mapメソッドは、Optional型にもあります。 Optional型に対しては、アンラップ せずに値を取得することができます。

let a: Int? = 10

let b = a.map { val in val * 10 } // Optional(100)

# flatMap

flatMap は、全ての要素を順番にアクセスし、それをクロージャの返り値を元にシーケンスに変換し、最終的に1つのシーケンスに変換します。

例えば、2次元配列を1次元の配列にすることができます。(フラットにする)

let a = [[1,2], [3,4], [5,6]]

// 2次元配列を1次元にする
let mapped = a.flatMap({ element in 
  return element
})

print(mapped) // [1, 2, 3, 4, 5, 6]

次の例は、渡された配列に新たな値を結合し、フラットにしています。

let a = [[1], [2], [3]]

let mapped = a.flatMap({ element in 
  // [1, 100]、[2, 200]、[3, 100] という配列の結果を返している
  return element + [100]
})

print(mapped) // [1, 100, 2, 100, 3, 100]

# mapとflatMapの違い

map は与えられた返り値を元に新たにシーケンスを生成するだけなので、上記の例を map で実行すると、次のようにフラットな配列にならずに処理されます。

let a = [[1,2], [3,4], [5,6]]

let mapped = a.map({ element in 
  return element
})

print(mapped) // [[1, 2], [3, 4], [5, 6]]

# comactMap

comactMap は、全ての要素に順番にアクセスし、引数のクロージャの戻り値がnilのものは無視されます。

let a = ["1", "2", "three", "4", "///5///"]

let mapped = a.compactMap { str in Int(str) } // [1, 2, 4]

クロージャの処理で、渡ってきた要素を Int?型 に型変換して、変換できなかったものはnilを返します。返り値を見ると、型変換に成功した要素だけが含まれているのがわかります。

同じような処理を map で実行すると、nilが含まれているのがわかります。

let a = ["1", "2", "three", "4", "///5///"]

let mapped = a.map { str in Int(str) } // [Optional(1), Optional(2), nil, Optional(4), nil]

# reduce

reduce は、要素を順番にアクセスし、引数のクロージャの戻り値のすべての値を一つにまとめます。

let a = [1,2,3,4,5,6,7,8,9,10]

let sum = a.reduce(0, { acc, element in 
  return acc + element
})

print(sum) // 55

初期値は、0 を設定しています。acc は、returnされた計算結果の値が入ります。順番に要素の計算をして、最終的にInt型の計算結果を返しています。

次の例では、文字列を連結させた結果を返しています。

let a = ["Hello", "How", "are", "you", "?"]

let str = a.reduce("", { acc, element in 
  return acc + " " + element
})

print(str) // Hello How are you ?

# lazy (LazySequence / LazyCollection)

lazy は、mapやfilterなどの処理のパフォーマンスを最適化されるために使われます。CollectionやSequenceの要素が必要になるときまで、評価しない仕組み (遅延評価) を提供しています。

# map や filter のパフォーマンス低下

通常、mapを使用すると新しいシーケンスを生成します。

let a = Array(1...1000)

let mapped = a.map { $0 * 2 }

また、filterも同様に新しいシーケンスを生成するので、次の例では、合計で3つのシーケンスを生成する処理が実行されています。

let a = Array(1...1000000)

// 1個目: a.map({ $0 * 2 })
// 2個目: a.map({ $0 * 2 }).filter({ $0 % 2 === 0 })
// 3個目: a.map({ $0 * 2 }).filter({ $0 % 2 === 0 }).map({ $0 / 2 })
let mapped = a.map({ $0 * 2 }).filter({ $0 % 2 == 0 }).map({ $0 / 2 })

最終的に、mapped 変数に入るのは一つの配列です。そのため、途中のシーケンス生成の処理は無駄になるので非効率になることがあります。

要素の数が少ないうちはあまり問題になりませんが、要素数が巨大になってくると、ひとつひとつのシーケンス処理がパフォーマンス低下に繋がる要因になります。

これを回避するために、 lazy を使って実装してみましょう。

# lazy でパフォーマンス最適化

lazy は、mapやfilterの直前に記述します。

let a = Array(1...1000000)

// lazy を map の前に記述する
let lazySeq = a.lazy.map { $0 * 2 }

let mapped = Array(lazySeq)

lazyは、内部で渡された配列とクロージャの処理をキャッシュしています。キャッシュすることにより、mapの処理を即時に評価せずに、必要な時だけ呼び出すように遅延評価してくれます。

上記の例では、Array(lazySeq) を実行したタイミングで、内部のクロージャの処理を評価しています。この遅延評価によって、巨大な要素数の配列でも複数のシーケンスを生成せずに、1回の実行で収めることができます。

さきほどの例を見てみましょう。mapやfilterを実行するごとに、新しいシーケンスを生成していましたが、lazyを使うことによって、最初の配列とクロージャの処理をキャッシュして、最後のArrayの変換処理だけで配列を1つ生成しています。

let a = Array(1...1000000)

// 配列とクロージャの処理をキャッシュ (まだ値を評価していない)
let lazySeq = a.lazy.map({ $0 * 2 }).filter({ $0 % 2 == 0 }).map({ $0 / 2 })

// このタイミングでキャッシュした値を遅延評価する
let mapped = Array(lazySeq)

# ベンチマーク

上記の例をもとに、ベンチマークをとってみましょう。

// lazyを使わない
let a = Array(1...100000000)
let mapped = a.map({ $0 * 2 }).filter({ $0 % 2 == 0 }).map({ $0 / 2 })

// lazyを使う
let a = Array(1...1000000)
let lazySeq = a.lazy.map({ $0 * 2 }).filter({ $0 % 2 == 0 }).map({ $0 / 2 })
let mapped = Array(lazySeq)

lazyを使わない

time ./swift-test-1

./swift-test-1 2.05s user 1.93s system 80% cpu 4.942 total

lazyを使う

time ./swift-test-2

./swift-test-2  0.86s user 0.71s system 81% cpu 1.927 total

lazyを使った方が大幅に改善されているのがわかります。

# Sequenceを実装する

Sequenceプロトコルに準拠することにより独自のSequenceを定義することができます。また、上記に挙げた、forEach, map, filter, reduceなどの関数や for-in文 も利用できるようになります。

# カウントダウンを実装

実際にカウントダウンをするSequenceを実装してみましょう。

struct Countdown: Sequence, IteratorProtocol {
    var count: Int
    mutating func next() -> Int? {
        defer { count -= 1 }
        return count == 0 ? nil : count
    }
}

makeIterator を実装することによってSequenceに準拠することができますが、この場合はデフォルトの実装を利用しています。

for-in文などで、動作確認することができます。

for i in Countdown(count: 3) {
  print(i)
}

// 3
// 2
// 1

Countdown(count: 3).forEach { print($0) }

// 3
// 2
// 1