NO MORE ビルド時間泥棒 ☕️❌【RxSwift編】

# はじめに
本内容は具体例の紹介のためにRxSwift編とありますがシチュエーションによってはSwift全体で言えることかもしれません。本内容で紹介する内容の効果はケースによりかなり差がでますので、そのケースの見極めのためのTIPSになります。

# RxSwiftとは
RxSwiftでは`Observable`に抽象化されたオブジェクトに対して宣言的な記述をすることで、その関係性と処理を決めることができます。

一方で内部的な抽象化の表現のために**ジェネリクス**などが多く用いられており、**メソッドチェーン**を多用します。そのため、書き方次第ではコンパイル時間が100倍違うケースも存在します。

# TL;DR

- `a == false`と`!a`の書き方の違いでコンパイル時間が`数千倍`変わるケースがある。

- オペレータ内で`Bool`の評価は論理値のみで評価する

```swift
.filter { $0.isOk == false } // Bad
.filter { !$0.isOk } // Good
.filter { $0.isOk } // Good
```
- 上記のように評価できない型(Int, Double, ...)は評価値を用意する

```swift
let limit: Double = 10
...
// $0がDouble型で渡ってくる場合
.filter { $0 <= 10 } // Bad
.filter { $0 <= limit } // Good
```
 
# なぜこう書いた方がいいのか?

まずは例を出しながらコンパイル時間が遅くなってしまうケースの紹介をしたいと思います。

例としてよく使う、かつ、内部でジェネリクスを多用している`Observable.combineLatest`で事例を紹介します。

`combineLatest`はどちらかの値が変更されたときに両方の値の最新をまとめて送出するオペレータで、かなり実用性が高いため頻繁に使用されます。

```swift
import UIKit
import RxSwift

class ViewController: UIViewController {

    let oi = Variable<Int>(0)
    let ob = Variable<Bool>(false)
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        // どちらかに変更があった時にoiが0、かつ、obがfalseの時に処理をしたい
        Observable.combineLatest(oi.asObservable(), ob.asObservable()) { $0.0 == 0 && $0.1 == false  }
            .subscribe(onNext: {
                print($0)
            })
            .disposed(by: disposeBag)
    }

}

```

一見どこにでもありそうなコードですがコンパイルしてみると`viewDidLoad`内だけで`2331.3ms`かかります。

Observableの型は指定してないですが5行のコードとは思えないコンパイル時間です。

このパターンの解決方法は以下の方法です。

```swift
import UIKit
import RxSwift

class ViewController: UIViewController {

    let oi = Variable<Int>(0)
    let ob = Variable<Bool>(false)
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        Observable.combineLatest(oi.asObservable(), ob.asObservable()) { $0.0 == 0 && !$0.1 }
            .subscribe(onNext: {
                print($0)
            })
            .disposed(by: disposeBag)
    }

}

```

コンパイル時間: `49.7ms`

`$0.1 == false`を`!$0.1`にするだけでコンパイル時間が`47倍`も違います。

## なぜこんなにもコンパイル時間に差が生じるのか?

しかし、そのまえにまずこの例においてコンパイル時間にほぼ**関係ない要因**を列挙します。

- 省略引数($0)を使用しているから
- Variableの値が`Int`と`Bool`の組み合わせだから
- `combineLatest`がstatic関数だから
- `A && B`と`B && A`のようなオペランド位置の違い

ということで上記のものは一見怪しそうに見えますが無罪です。

そして気になる答えは...

リテラル`(Literal)`で評価をしているからです。
これがもっともコンパイル時間に寄与しています。[^1]

リテラルとは`10`や`true`や`nil`などを指します。

```swift
let isOk = false // `false is a boolean literal`
let num = 10 // `10` is a integer literal
```
この場合の`$0.1 == false`の`false`もリテラルです。
勘違いされがちなケースとしては`true`や`false`はBool型であるという認識です。
また同様に`10`などもInt型ではありません。
正しい認識としては`false`はBool型として受けれられるということです。
また`let isOk = false`のように型情報が明記されない場合にはデフォルトで`Bool型`に解釈されるというだけです。

つまり何が言いたいのか?

- リテラルは型情報をもっていない
- リテラルを型にはめるにはコンパイラがコンテキスト(文脈)を読む必要がある

ここまでくると`$0.1== false`と`!$0.1`でコンパイル時間が異なる理由がわかります。

`combineLatest`を例にすると、まず宣言は以下のようになっています。

```swift
public static func combineLatest<O1: ObservableType, O2: ObservableType>
        (_ source1: O1, _ source2: O2, resultSelector: @escaping (O1.E, O2.E) throws -> E)
            -> Observable<E> 
```

## `$0.1 == false`が遅い理由
まず`$0.1 == false`は$0で使用されるジェネリクスの`ObservableType`の解釈から始まり`associatedtype`である`E`の型が判明されるまで`false`が何型なのか決定することはできません。
`$0.1`の型が決定してからはじめて`false`は変換イニシャライザを通して`Bool型`になることで比較(==)が可能となります。
よって`$0.1`が判明したあとに再度`false`の型推論が待ち受けているということになり、これがコンパイル時間を長くする要因だと考えられます。

## `!$0.1`が早い理由
一方`!$0.1`での評価はどうでしょうか?
まず、真の評価をしたいときには`$0.1`だけを記述することで`$0.1`が`Bool型`と判定後に、すぐに評価が行えます。
次に、偽の評価をしたいときに使われる`!`(論理反転演算子)は**Bool型のstatic関数**です。

```swift
prefix public static func !(a: Bool) -> Bool
```
いずれの場合も`Bool型`と判定された場合にはそれ以上、型推論やプロトコル準拠を確認する必要がないということになりコンパイル時間が短くなったと考えられます。

このような点を考慮すると`Bool型`に関してはリーダビリティもあまり変わらないため一貫して論理値で評価するのが良いと思います。

# テストしてみる
コンパイル時間を長くしてしまうケースと今回の解決策でどれくらいトータルのビルド時間が変わるか試してみました。
例で出した`combineLatest`だけが特別というわけでもなくメソッドチェーンなどにある何気ない`filter`や`map`も対象になります。

## 型推論される式でリテラルを多用する

```swift
Observable.combineLatest(oi.asObservable(), ob.asObservable()) { $0 }
            .filter { $0.1 == true }
            .filter { $0.1 == true }
            .filter { $0.1 == true }
            .filter { $0.1 == true }
            .filter { $0.1 == true }
            .subscribe(onNext: {
                print($0)
            })
            .disposed(by: disposeBag)

```
`199958.1ms` -> 3分10秒 :scream: 

## 型情報のある評価値で評価する 
```swift
let isOk = true
Observable.combineLatest(oi.asObservable(), ob.asObservable()) { $0 }
            .filter { $0.1 == isOk }
            .filter { $0.1 == isOk }
            .filter { $0.1 == isOk }
            .filter { $0.1 == isOk }
            .filter { $0.1 == isOk }
            .subscribe(onNext: {
                print($0)
            })
            .disposed(by: disposeBag)
```
`46395.5ms` -> 46秒  :fearful: 

## 論理値だけで評価する

```swift
Observable.combineLatest(oi.asObservable(), ob.asObservable()) { $0 }
            .filter { $0.1 }
            .filter { $0.1 }
            .filter { $0.1 }
            .filter { $0.1 }
            .filter { $0.1 }
            .subscribe(onNext: {
                print($0)
            })
            .disposed(by: disposeBag)
```
`29.1ms` -> 0.029秒 :sunglasses: 

この場合で言えば論理値で評価するのとリテラルで評価するのではコンパイル時間に`6500倍`の差が出ます。(極端な例ではありますが)

# まとめ

これらの内容はRxSwiftに限った内容ではないですが、RxSwiftを利用してコーディングしてるときには、こんな些細なことでコンパイル時間が変わるんだ程度に覚えといていただけたら幸いです。

ジェネリクスなども使用しないケースでは実際`a == false`も`!a`も気になる程コンパイル時間に差はありませんので、特に好きな方で記述していいと思います(`!a`とした方がわずかに早くなりますが)。ですが、プロジェクトなどでコーディング規約などとなると、こういったケースが存在するとなると`if a == true {}`などの書き方は`if a {}`で統一しておくのが無難かと思います。

塵も積もればコーヒータイムとなってしまいます。特に`RxSwift`を導入しているプロジェクトなどでは一度ビルド時間の調査を行ってみてこのようなケースに陥ってないか確認してみるのもいいかもしれません。

最後まで見ていただき、ありがとうございました。


参考:

- [Swift language documentation: Literal](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/LexicalStructure.html#//apple_ref/doc/uid/TP40014097-CH30-ID414)
- [BuildTimeAnalyzer-for-Xcode](https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode)
- [XCodeコンパイル時間短縮](https://developers.cyberagent.co.jp/blog/archives/7815/)
- [型推論でビルドに時間がかかっている場合の解決法](http://qiita.com/marty-suzuki/items/a9019e602567e7a1ab48#_reference-66b8eb9aee07f174754d)
- [リテラルと型の話](https://www.slideshare.net/tomohirokumagai54/swift-56311063)
- [RxSwift](https://github.com/ReactiveX/RxSwift)
- [Improve compile time in RxSwift](https://medium.com/@ryo31330023/swift-improve-compile-time-in-rxswift-dba228778b35)

[^1]: 他にも`&&`などで評価を連結した場合や配列を`+`で連結した場合にその連結数によって指数的にコンパイル時間が長くなります。