RxSwift.Driver についての個人的見解

この投稿は、[AbemaTV Advent Calendar 2017](https://adventar.org/calendars/2216) 18日目の記事です。

こんにちは、稲見 ([@inamiy](https://github.com/inamiy)) です。
今年の3月に [AbemaTV](https://abema.tv/) にジョインして、iOSエンジニアをしています。
普段、業務では [RxSwift](https://github.com/ReactiveX/RxSwift) を使っていますが、本家の [ReactiveX](http://reactivex.io/) に存在しない独自拡張である [`Driver`型](https://github.com/ReactiveX/RxSwift/blob/4.0.0/RxCocoa/Traits/Driver/Driver.swift) に関して常々思うところがあり、今回、自分の考えを整理してみたいと思います。

## 復習：`Driver`について

1. エラーを送らない
2. メインスレッドでの送信保証
3. 内部で `share(replay: 1, scope: .whileConnected)` を行う
	- 基本動作は `ReactiveX.shareReplay(1)` と似ている
		- `shareReplay(1) = multicast(ReplaySubject(bufferSize: 1)).refCount()`
	- ただし、実際の作りは `ConnectableObservable` + `ReplaySubject` を中継に挟まない
	-  `observer` の数が0になった時、内部キャッシュ (`ShareReplay1WhileConnectedConnection`) を破棄し、数が再び1になったら、再生成する
		- `shareReplay(1)` で 内部キャッシュの `ReplaySubject` が破棄されないのとは対照的
		- メモリ効率の良いHot変換
		- 過去の古いキャッシュに引きずられないメリット

`Driver` について良く知られている性質は、大体この通りだと思います。
（他言語のRxユーザーは、 `share(replay: 1, scope: .whileConnected)` の挙動に若干戸惑うかもしれません[^1]）

さて、あまり知られていないかもしれませんが、`Driver`には下記の問題点[^2]があります。
順に見ていきましょう。

1. `Driver` は、必ずしもHotではない
2. `Driver` のRxオペレータは、独自実装できない
3. `Driver` のメソッドチェーンは、メモリを大量消費する

## 1. `Driver` は、必ずしもHotではない

早速ですが、RxSwift v4.0.0 で次の単純なコードを実行してみます。

```swift
let o = Observable.of(1, 2)
    .map { x -> Int in
        let now = Date().timeIntervalSince1970
        print("[map] \(x) -> \(now)")
        return x
    }
    .share(replay: 1, scope: .whileConnected)

print("--- 1 ---")
o.subscribe()
print("--- 2 ---")
o.subscribe()
```

結果はこうなります。

```
--- 1 ---
[map] 1 -> 1513433642.38432
[map] 2 -> 1513433642.38482
--- 2 ---
[map] 1 -> 1513433642.38628
[map] 2 -> 1513433642.38645

```

なんと、 **値がまったくシェアもリプレイもされていません**。
これでは、Cold Observableと振る舞いが変わらないですね。
（もちろん、`share(...)`を`asDriver(...)`に置き換えても同じ）

どうしてこうなったかは、[ShareReplayScope.swift](https://github.com/ReactiveX/RxSwift/blob/4.0.0/RxSwift/Observables/ShareReplayScope.swift#L202)を読むとすぐに分かります。
Observerの数が0になると呼ばれる`_synchronized_dispose()`が、上流の`onError`/`onCompleted`時にも呼ばれているからです。
すなわち、 **川の最上流にColdを持ってくると、その`onError`/`onCompleted`直後に`Driver`はColdとして振る舞います**。

余談ですが、この挙動は `.whileConnected` であれば、 `replayCount = 1` 以外でも同様に起こります。
RxSwift v4.0.0以降では、 **`share()` を使ってHot変換したコードはすべて上記の振る舞いに化けます**ので、ご注意下さい。
（ただし、通常の用途で、例題のようなエッジケースが発生することは稀だと思います）

- 参考：[Change `share(replay:scope:)`'s default scope to `.forever` by inamiy · Pull Request #1527 · ReactiveX/RxSwift](https://github.com/ReactiveX/RxSwift/pull/1527)

## 2. `Driver` のRxオペレータは、独自実装できない

これは `Driver` に限らず、 **`SharedSequence` 全般について、Rxオペレータを独自で実装することができません** 。

理由は、[SharedSequence.swift#L30-L48](https://github.com/ReactiveX/RxSwift/blob/4.0.0/RxCocoa/Traits/SharedSequence/SharedSequence.swift#L30-L48) を見ると分かるように、

- `(internal) init` なので、外部公開されておらず、`Driver`を生成することが難しい
- `SharedSequence.createUnsafe()`があるけど、`#if EXPANDABLE_SHARED_SEQUENCE` マクロからして、とても使える感じがしない

からです。

「いやいや、`asDriver()`があるから余裕でしょ」と思われる方は、実際に`Driver.map`を自作してみましょう。
**多相的関数の中で、`asDriver(/* NoError変換 */)`の使用が、型システムによって阻まれる** 様子が分かります。

「そこで`ControlEvent`を経由して`.asDriver(/* 引数なし */)`ですよ」と言える方は、なかなか勘が鋭いです。
が、その発想は、`ControlEvent`の非安全なイニシャライザの弱点を突いているほか、余計な`subscribeOn()`も付いてしまったり、やはり賢明なアプローチとは言えません。

## 3. `Driver` のメソッドチェーンは、メモリを大量消費する

これが見落としがちで一番重要な点ですが、 **「`Driver` を1つ生成 → `drive (subscribe)`」する度に、シェア用の内部キャッシュが1つ生まれている** ことを忘れてはいけません。

つまり、

```swift
let o = source
    .asDriver(onErrorDriveWith: .empty())
    .map { ... }
    .map { ... }
    .map { ... }
    ...
```

は、実質

```swift
let o = source
    .share(replay: 1, scope: .whileConnected)
    .map { ... }.share(replay: 1, scope: .whileConnected)
    .map { ... }.share(replay: 1, scope: .whileConnected)
    .map { ... }.share(replay: 1, scope: .whileConnected)
    ...
```

に相当し、 **`Driver` の各種Rxオペレータを多用すればするほど、`drive`時にその数だけのコピーが大量発生**します（`Observable.E`が値型の場合）。

もちろん、FRP（関数型リアクティブプログラミング）自体が、各々のRxオペレータに状態（内部キャッシュ）を閉じ込めて副作用を気軽に扱うツールである以上、富豪プログラミングの一種ですし、良識のある社会人なら多少非効率的であっても目を瞑るものですが、 **全く活用されないキャッシュを作るのは、たとえ個々がbyte単位で軽量であっても無駄** です。

なので、良識のない個人的な意見としては、 **`Driver` のRxオペレータは一切使わず、UIバインディング (`drive`) だけに専念する** 使い方が一番賢いと思います。

なお、この **「Hot化する際に（`ConnectableObservable`を含めた様々な）状態を必要とする設計」**は、RxSwiftに限らず、 **ColdとHotを型レベルで区別しない、すべてのFRPフレームワークに共通する問題**なので、広く一般認識として持っておくと良いでしょう。

ちなみにSwiftには既に、ReactiveXの概念をさらに発展させて、Cold/Hotを型で区別した [より効率の良いフレームワーク](https://github.com/ReactiveCocoa/ReactiveSwift)が存在します。
詳しくは下記のポエム（英語）を読んで、効率の違いを確かめてみて下さい。

- https://gist.github.com/inamiy/31bd4c3480f9a4a6c2e945c211c7fed0 

## まとめ

`RxSwift.Driver` について、思いの丈を綴ってみました。

[^1]: 確認した範囲では、[`RxJS.shareReplay`](https://github.com/ReactiveX/rxjs/blob/d5ced68ae3349477800f0a186dc7285e135ada69/src/operators/shareReplay.ts)は、昔は`multicast`ベース、現在は`.whileConnected`動作が仕様のようでした。
[^2]: これらを些細な問題とみるか、設計ミスと捉えるかは、個人の自由です。