SwiftのコンパイラでThe compiler is unable to type-checkのエラーが出た時は、型を明示するといいかもしれない

f:id:monex_engineer:20200302112403j:plain

証券企画室の佐藤です。
1月からクライアントアプリ開発チームに弟子入りして、Swiftのコードを書いています。
そしてRxSwiftのcombineLatestとdistinctUntilChangedをメソッドチェーンで繋げて使った時に The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions といったコンパイルエラーに悩まされたので、対処の仕方を書きたいと思います。
なお、発生したのはXcode11.3、RxSwift5.0.1です。

解決策

最初に対処方法を簡単に書くと、型が曖昧だと思うところを探して明示する、です。
いきなりこう書くと当たり前のように聞こえますが、Swiftは型推論を駆使する言語なので、Swiftの書き始めはどこまでコンパイラに任せていいのかわからないことが多いです。
根本的なところではSwiftではより短いシンプルな記述を心がけましょう、というところに尽きますが、 RxSwiftを使う中で3~4つ程度のメソッドチェーンを組み合わせるだけでも発生しうるので、一つの対処法を示せればと思います。
また、式を出来るだけ短くすることも有効ですが、こちらはWeb検索するといくつか記事が出てくるので、そちらの詳細については記述しません。

再現コード

今回起きた問題を再現するコードがこちらです。
GitHub上のRxSwiftリポジトリをチェックアウトしてきて、Rx.playgroundに記述することで実験できます。
これを実行すると、disposedの後ろにコンパイルエラーが表示されて、実行に失敗します。

コンパイラ型推論を難しくするポイントがこちら。

  • (1) RelayにBoolやIntのようなプリミティブではないオブジェクトを流す。
  • (2) combineLatestの呼び出しの中でmapを呼び出す。
  • (3) combineLatestのソースを3つ以上にする。
import RxSwift
import RxCocoa

let disposeBag = DisposeBag()

class ClassA {
    public let value: Bool
    
    init(_ value: Bool){
        self.value = value
    }
}

let relayA = BehaviorRelay<ClassA>(value: ClassA(true)) //(1)
let relayB = BehaviorRelay<Bool>(value: true)
let relayC = BehaviorRelay<Bool>(value: true)

Driver.combineLatest(
    relayA.asDriver().map{return $0.value}, //(2)
    relayB.asDriver(),
    relayC.asDriver() //(3)
)
.distinctUntilChanged{(old, new) -> Bool in //コンパイラが型推論できないところ。oldとnewが(Bool,Bool,Bool)であることがわからない。
    return old.0==new.0 && old.1==new.1 && old.2==new.2
}
.drive(onNext: { (valueA, valueB, valueC) in
    print("A: " + String(valueA) + " B:" + String(valueB) + " C:" + String(valueC))
})
.disposed(by: disposeBag) //The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions

relayB.accept(false)
relayB.accept(true)

出来るだけ記述を変えずにコンパイルを通すには、下記のようにdistinctUntilChangedの呼び出しでoldとnewの型がBoolのタプルであることをコンパイラに教えてあげれば良いです。

.distinctUntilChanged{(old:(Bool, Bool, Bool), new:(Bool, Bool, Bool)) -> Bool in

まとめ

RxSwiftのdistinctUntilChangedに限って言えば、Equatableに準じたクラスまたは構造体を定義して、combineLatestの後にmapを書いて変換してやるのも一つの手であり今回の案件ではそれで対応しましたが、 Swiftコンパイラ型推論が機能できないエラーが出た時には、

  • 式を程よく短くする
  • 型が曖昧な変数に対して、型を明示する
    といったことがコンパイラにとっても人にとっても読みやすいコードになる対処法です。

佐藤 俊介 証券企画室