Swift 1.2で導入されたflatMapのユースケース7個を実際のプロジェクトから抜き出してみた

Swift 1.2 が正式にリリースされ、 __`flatMap` が追加されました__。僕は _Extension_ を使って `flatMap` を[独自に実装し](https://github.com/koher/swiftf)[^1]、利用していたため、これまでも実ブロジェクトで `flatMap` を使ってきました。

本投稿は、 __僕が携わった実際のアプリのコードから `flatMap` を使っている箇所を抜き出し、その使い方を紹介する__ ものです。

なお、コードを抜き出すに当たって問題がないように若干修正してあるので完全に元のままではありません。また、実プロジェクトからの抜粋なのでユースケースとして網羅的でないことをご了承下さい。

`flatMap` は `Array` だけでなく `Optional` のメソッドでもあるので、以下セクションを分けて記載します。

## データ構造

初めに簡単にこのアプリで扱うデータ構造について説明します。今回取り上げるアプリでは、 `Chapter`, `Section`, `Page`, `Content`, `Resource` が入れ子になるデータ構造を扱っています。

`Chapter` は `sections: [Section]` を持っていて、 `Section` は `pages: [Page]` を持っていて…、というシンプルな構造です。

```swift
// これは Swift のコードではありません。データ構造を表しているだけです。
Chapter { sections: [Section] }
Section { pages: [Page] }
Page    { contents: [Content] }
Content { resources: [Resource] }
```

## Array

### case 1: ある Chapter に属するすべての Resource を取り出し、それをサーバから一括ダウンロードするために NSURL に変換する

```swift
chapter.sections.flatMap { $0.pages }.flatMap { $0.contents }.flatMap { $0.resources }.map { toUrl($0) }
```

`Chapter` の持つ `[Section]` を `[Resource]` に変換するために `flatMap` を連続して使い、最後に `map` で `[Resource]` を `[NSURL]` に変換しています。このように、 `flatMap` は __入れ子になった構造をフラットにしてデータを取り出す__ ために使えます。

ここでキモになるのは、単に入れ子の `Array` をフラットにするのではなく、フラットにするのと同時に任意の変換が可能が点です。上記では、 `sections.flatMap { $0.pages }` のように、 `Section` を `[Page]` に __変換した上で結合__ しています。これが __flatMap = flatten + map__ という名前の理由です。

もしこれを `flatMap` ではなく `map` でやると `Array` が入れ子になってしまい、結果が `[NSURL]` ではなく `[[[[NSURL]]]]` になってしまいます。

### case 2, 3: UITableView で Section に属するすべての Content を表示する

__※最初に掲載したコードはデータ件数が多いときにパフォーマンスが劣化するため、サンプルとして望ましくないと思い修正しました。その結果、 case 2 と case 3 が一つの case になってしまいました。__

```swift
private let sectionContents: [[Content]] = sections.map { $0.pages.flatMap { $0.contents } }

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
	return sectionContents[section].count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
	let content = sectionContents[indexPath.section][indexPath.row]
	...
}
```

こちらも、 `Page`, `Content` という入れ子構造をフラットにするために `flatMap` を使っています。

通常、 `UITableView` では 1 階層ずつ潜っていくことが多いですが、 `Page` に一つしか `Content` がないことも多く、 `Page` のために一つ階層を作ってしまうと冗長だったため `Page` の階層を潰してフラットにしました。

また、 `UITableView` の _section_ を使って `Section` と `Content` の 2 階層分を一つの `UITableView` で表示しているので、 `[Content]` ではなく `[[Content]]` になるように、間の `Page` の階層だけをフラットにしています。

### case 4: ID で Page を引くことができる Dictionary を作る

```swift
chapters.flatMap { $0.sections }.flatMap { $0.pages }.reduce([:]) {  (var result, page) in
	result[page.id] = page
	return result
}
```

元のデータ構造では `Page` が異なる `Section` にバラバラに保持されていますが、そのままでは `Page` の ID で `Page` を引きたいときに不便です。 `flatMap` で `[Chapter]` を `[Page]` に変換し、その後 `reduce` で `[String: Page]` （ `String` は `Page` の ID ）を生成しています。


### case 5: JSON のパースに失敗した要素を含まない Array を作る

```swift
json["beacons"].array?.flatMap {
	let major: NSNumber? = $0["major"].number
	let minor: NSNumber? = $0["minor"].number
	let id: String? = $0["id"].string

	// どれか一つでも nil なら結果を nil にする。 nil になってしまった場合は空配列に置き換えて結果から除去する。
	return (Beacon.construct <%> uuid <*> major <*> minor <*> id).map { [$0] } ?? []
}
```

上記のコードはやや複雑です。やりたいことは次のような処理と同じようなことです。

今、 `["2", "3", "four", "5", "six", "7"]` という `Array` があるとします。この各要素を `toInt` で `Int` に変換したいとします。しかし、 `"four"` や `"six"` は当然変換に失敗します。変換に失敗した要素は結果から取り除き、 `[2, 3, 5, 7]` という `Array` を得るにはどうすれば良いでしょうか。

このようなケースでは単純な `map` による変換は使えません。また、変換と条件判定が同時に行われるので `filter` も不適切です。通常は次のように `reduce` を使って処理します。

```swift
["2", "3", "four", "5", "six", "7"].reduce([Int]()) { (var result, element) in
	if let number = element.toInt() {
		result.append(number)
	}
	return result
}
```

しかし、 `reduce` を使ったコードを書くのは面倒です。パフォーマンスが重要な箇所でなければ、 `flatMap` を使って次のように書くこともできます。

```swift
["2", "3", "four", "5", "six", "7"].flatMap { (element: String) -> [Int] in
	if let number = element.toInt() {
		return [number]
	} else {
		return []
	}
}
```

つまり、要素の変換に成功したら 1 要素の `Array` を、失敗したら空の `Array` を `return` して結合させることで、変換に失敗した要素を取り除くことができるのです。

これをワンライナーで書くと次のようになります。

```swift
["2", "3", "four", "5", "six", "7"].flatMap { $0.toInt().map { [$0] } ?? [] }
```

最初に挙げた JSON のパースのコードはこれと同じことをやっています。 `major`, `minor`, `id` のどれか一つでもパースすることができなければ `Beacon.construct` に失敗して `nil` が `return` されます（ `<*>` は[アプリカティブスタイル](http://qiita.com/koher/items/95583f58a443f6a33c28)です）。

## Optional

### case 6: String? で String がキーの Dictionary を引く

```swift
pageIdOrNil.flatMap { pageIdToPage[$0] }
```

`pageIdOrNil` は `String` ではなく `String?` なので、そのままでは `pageIdToPage[pageIdOrNil]` のように使えません。そこで、 `flatMap` を使って `Optional` の中身を取り出し `Dictionary` のキーとして利用します。

`map` でも同じことができるように思えるかもしれませんが、 `Dictionary` をキーで引いた戻り値は値の `Optional` になるので、このケースだと `map` を使うと戻り値の型が `Page?` ではなく `Page??` になってしまいます。 `Optional` についても、 `flatMap` は入れ子になった `Optional` を __変換と同時にフラットにする__ 存在です。 `flatMap` は `Array` も `Optional` も中身を変換してフラットにしてくれます。 `Array` と `Optional` の類似性については、 ["ArrayとOptionalのmapは同じです"](http://qiita.com/koher/items/68585cff691d87124077) をご覧下さい。

`Optional` な値を取りあつかうには _Optional Chaining_ `foo?.bar` が便利ですが、上記のようなパターン `foo.flatMap { bar($0) }` ではうまくいきません。 _Optional Chaining_ で書けることはすべて `flatMap` で書くことができるので、 _Optional Chaining_ は `flatMap` の __一部のケースを便利に書くための方法__ と考えておくと良いです。

詳しくは ["SwiftのOptional型を極める"](http://qiita.com/koher/items/c6f446bad54442a28bf4) をご覧下さい。

なお、 `flatMap` を使わずに上記の処理を書こうとすると次のようになり、とても面倒です。

```swift
let page: Page?
if let pageId = pageIdOrNil {
	page = pageIdToPage[pageId]
} else {
	page = nil
}
```

### case 7: String が引数のイニシャライザに String? を渡す

```swift
json["uuid"].string.flatMap { NSUUID(UUIDString: $0) }
```

これも __case 6__ と同様のケースです。 `String?` のままでは引数として使えないので `flatMap` を使って中身を取り出してイニシャライザに渡しています。このイニシャライザの戻り値の型は `NSUUID?` なので、ただの `map` では戻り値の型が `NSUUID??` になってしまうので `flatMap` を使います。

### おまけ: Int? と Int? を足す

本文の修正で __case 2__ と __case 3__ が一つになってしまい、ユースケースが 6 個になってしまったので、「実際のプロジェクトから抜き出して」いませんが、 `flatMap` が使える他のケースを挙げておきます。

Swift を使っていると `Optional` だらけになってしまいますが、 `Int?` 同士を足そうとするだけでも `if let` で分岐してだと大変です。 `flatMap` を入れ子にすれば次のように 1 行で計算することができます。

```swift
let a: Int? = 2
let b: Int? = 3

a.flatMap { a0 in b.flatMap { b0 in a0 + b0 } } // 5

let c: Int? = nil

a.flatMap { a0 in c.flatMap { c0 in a0 + c0 } } // nil
```

----------------

[^1]: `bind` とか他の名前もあり得ましたが、たまたまシグネチャが完全一致しました。