40
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[RxSwift] flatMapの呼び出しを恐がりたくない人生だった

Last updated at Posted at 2019-02-03

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の話が食い込んでくることの**???感**といったらありませんでした。

この記事を書くことで、自らに巣食っているこの恐怖を少しでも克服していきたいと思います。

恐さを克服する

まず、mapflatMapもオペレーターの一種であることに変わりはなく、
「新しいObservableを返す」という点で目的は共通していることがわかると、いくばくか落ち着きを取り戻せます。
私が大好きないつものやつ**「関数のシグネチャを確認」**をここで早くも投入してみましょう。
mapの定義flatMapの定義を確認してみます。

map
public func map<R>(_ transform: @escaping (E) throws -> R) -> Observable<O.E>
flatMap
public func flatMap<O: ObservableConvertibleType>(_ selector: @escaping (E) throws -> O) -> Observable<O.E>

mapflatMap双方、返り値はObservable<O.E>で共通しています。
flatMapといえど、RxSwiftの数多あるオペレータの一種であることに変わりはないことが、いまいちど確認できました。

そして、flatMapが引数として取るクロージャが遵守すべき型の約束はどうなっているでしょうか?

flatMap(再掲)
public func flatMap<O: ObservableConvertibleType>(_ selector: @escaping (E) throws -> O) -> Observable<O.E>
  1. 型パラメータとその型制約<O: ObservableConvertibleType>
    と、
  2. 引数としてのクロージャ(selector)の返り値の型O

に着目ください。
この2点を総合すると、**「クロージャ(selector)は返り値としてObservable(ConvertibleType)を返さないとだめだよ!」**という指定がなされていることがわかります。

つまり、このようなコードは書けません。

let result: Observable<String> = Observable.of("梅", "竹", "松")
    .flatMap { text -> String in  // 返り値はString(= Observable(ConvertibleType)ではない)から🙅‍♀️
        return "\(text)☆"
    }

クロージャの内部に書かれる処理が得てして複雑になるのも、
Observable(ConvertibleType)を作って返さないといけないのだから、ある意味当然だったというわけですね...。

この際ですので、ついでにmapが引数として取るクロージャのシグネチャも見てみましょう。

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)   // バリデーション成功
        }
    }
}
flatMapのクロージャ内で使用する
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を使っても書けるのでは?

書けます。結果の型がネストしてもいいのであれば

冒頭の例において、flatMapmapに書き換えてみると...

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()

ObservableObservableでネストしてしまう結果になってしまいました...。

この挙動は、
↓のように、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アプリ設計パターン入門

40
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?