flatMap
の呼び出しが理解不能だったプログラマの記録です。(ビギナー向け)
この記事だけでflatMap
の挙動が把握できるようにはならないのですが、
私のような悩める人に対し、違った角度からの見方を提供できたならば僥倖です...。
環境
Xcode 10.1(10B61)
Swift 4.2.1
RxSwift 4.4.0
flatMapの呼び出しが理解不能だった
map
のほうはまだやってることわかるんですよ。
ただ、flatMap
となると...あいつなんなの?
ただでさえオペレータのメソッドチェーンを追ってるだけでも脳メモリに負荷がかかっているのに、
引数のクロージャの中で、なぜいきなり仰々しい構文が途中で入ってくるのか...こわい...
let result: Observable<Bool> = userNameTextField.rx.text
.orEmpty
.asObservable()
.skip(1)
.debounce(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged() // ここまで1つのObservableの話をしていたと思っていたのに...
.flatMap { userName in
return userNameValidator.validate(text: text) // なんかいきなり別のObservableの話が登場してきてこわい
.observeOn(MainScheduler.instance)
.catchErrorJustReturn(false)
}
.share()
1つのObservable
の話をしていたと思いきや、
新しいObservable
の話が食い込んでくることの**???感**といったらありませんでした。
この記事を書くことで、自らに巣食っているこの恐怖を少しでも克服していきたいと思います。
恐さを克服する
まず、map
もflatMap
もオペレーターの一種であることに変わりはなく、
「新しいObservableを返す」という点で目的は共通していることがわかると、いくばくか落ち着きを取り戻せます。
私が大好きないつものやつ**「関数のシグネチャを確認」**をここで早くも投入してみましょう。
mapの定義とflatMapの定義を確認してみます。
public func map<R>(_ transform: @escaping (E) throws -> R) -> Observable<O.E>
public func flatMap<O: ObservableConvertibleType>(_ selector: @escaping (E) throws -> O) -> Observable<O.E>
map
とflatMap
双方、返り値はObservable<O.E>
で共通しています。
flatMap
といえど、RxSwiftの数多あるオペレータの一種であることに変わりはないことが、いまいちど確認できました。
そして、flatMap
が引数として取るクロージャが遵守すべき型の約束はどうなっているでしょうか?
public func flatMap<O: ObservableConvertibleType>(_ selector: @escaping (E) throws -> O) -> Observable<O.E>
- 型パラメータとその型制約
<O: ObservableConvertibleType>
と、 - 引数としてのクロージャ(selector)の返り値の型
O
に着目ください。
この2点を総合すると、**「クロージャ(selector)は返り値としてObservable(ConvertibleType)を返さないとだめだよ!」**という指定がなされていることがわかります。
つまり、このようなコードは書けません。
let result: Observable<String> = Observable.of("梅", "竹", "松")
.flatMap { text -> String in // 返り値はString(= Observable(ConvertibleType)ではない)から🙅♀️
return "\(text)☆"
}
クロージャの内部に書かれる処理が得てして複雑になるのも、
Observable(ConvertibleType)
を作って返さないといけないのだから、ある意味当然だったというわけですね...。
この際ですので、ついでにmap
が引数として取るクロージャのシグネチャも見てみましょう。
public func map<R>(_ transform: @escaping (E) throws -> R) -> Observable<O.E>
引数としてのクロージャ(transform)の返り値の型R
を見て分かる通り、
「Observable(ConvertibleType)
を返してね!」というflatMap
のときとは対照的に、クロージャの返り値の型に制約は設けられていないことが確認できます。
先ほどのflatMap
の例では書けなかったコードも、map
であれば当然のようにOKです。
let result: Observable<String> = Observable.of("梅", "竹", "松")
.map { text -> String in // 🙆♀️
return "\(text)☆"
}
Observableをreturnする形で書けると何が嬉しいのか
自身や、自身が保有するモデルクラスが持つObservableを返すメソッドを呼び出すことで、
flatMap
が引数に取るクロージャの要件を満たすことができます。
冒頭の例を取るなら...
struct UserNameValidator {
func validate(userName: String) -> Observable<Bool> {
if ... /* なんらかのバリデーションロジック */ {
return .just(false) // バリデーション失敗
} else {
return .just(true) // バリデーション成功
}
}
}
let validator = UserNameValidator()
userNameTextField.rx.text
.orEmpty
.asObservable()
.skip(1)
.debounce(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMap { userName in
return validator.validate(text: text) // 自身が保有するモデルクラスのメソッド呼び出し
.observeOn(MainScheduler.instance)
.catchErrorJustReturn(false)
}
.share() // もちろん後続でチェーンもできる
バリデーションを担うクラスのメソッドを呼び出す形でflatMap
が引数として取るクロージャをしっかり充足させ、flatMap
を呼んています。
こうした形を取ることで、責務を適切に分割した設計に近づけやすくなる可能性などが考えられます。
逆に言えば、モデルクラスはObservableを返すようにメソッドを定義してあげると、
その利用者側は後続のオペレータにチェーンしてもらえたりなど扱いやすくなる、といったメリットが考えられます。
(iOSアプリ設計パターン入門 第7章 MVVM〜Modelの項に記載されていました。)
筆者が知覚しているメリットはこれだけですが、他にもたくさんありそうです。
同じことはmap
を使っても書けるのでは?
書けます。結果の型がネストしてもいいのであれば。
冒頭の例において、flatMap
をmap
に書き換えてみると...
let result: Observable<Observable<Bool>> = usernameTextField.rx.text // 型がネストしてしまった🤣
.orEmpty
.asObservable()
.skip(1)
.debounce(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.map { [unowned self] text in // なぜならここをflatMapからmapに変えてしまったからね...
return self.validate(text: text)
.observeOn(MainScheduler.instance)
.catchErrorJustReturn(false)
}
.share()
Observable
がObservable
でネストしてしまう結果になってしまいました...。
この挙動は、
↓のように、String
を返すクロージャを取るmap
の結果 => Observable<String>
になる、のだから...
let result: Observable<String> = Observable.of("梅", "竹", "松")
.map { text -> String in
return "\(text)☆"
}
↓のように、Observable<String>
を返すクロージャを取るmap
の結果 => Observable<Observable<String>
になる、というわけですね。
let result: Observable<Observable<String>> = Observable.of("梅", "竹", "松")
.map { text -> Observable<String> in
return Observable.just("\(text)☆")
}
そしてこのネストされたObservableを購読したいとなると、目もあてられない感じになってしまいます。
result.subscribe(onNext: { innerObservable in
innerObservable.subscribe(onNext: { text in
print(text)
})
.disposed(by: self.bag)
})
.disposed(by: bag)
flatMap
を使えば、Observableでネストすることなく結果を返してくれます。
let result: Observable<String> = Observable.of("梅", "竹", "松")
.flatMap { text -> Observable<String> in
return Observable.just("\(text)☆")
}
もっとflatMapと仲良くなりたいな
この記事では一切触れなかった切り口である
元の Observable のイベントを Observable に変換した上で、その発行するイベントをマージします。
という点に言及しているこちらなどを参考にさせてもらっています。
他にもこちらにあるような、
flatMapやflatMapLatestはクロージャ内で生成されたオブザーバブルにCompletedが流れても、それをまとめた出力にはCompletedが流れない
という点も見逃せないですね。
これを知ってからマーブル図を見直すと、確かにflatMap
のクロージャに書かれているonCompleted
(縦線)が、合成後のストリーム上では流れて(書かれて)いないことに気づけました...。
おわりに
初めて筆者がRxSwift
に触れてからすでに1年半を数えますが、
flatMap
だけが、というよりRxSwift
自体、全然自信が湧かないのです。難しいよ...😂
どうかこの記事が私のような悩める人に対し、違った角度からの見方を少しでも提供できていたら...と願っています🙏
参考書籍
・モバイルアプリ開発エキスパート養成読本
・RxSwift研究読本 Ⅰ
・iOSアプリ設計パターン入門