"ReactiveCocoa vs RxSwift"の和訳
https://www.raywenderlich.com/126522/reactivecocoa-vs-rxswift
-
Functional Reactive ProgrammingはSwift開発者の間でますます人気が高まってきている方法論である。それは複雑な非同期処理のコードを書きやすく、理解しやすくすることができる。
この記事では、FRPの中で人気のあるライブラリ、RxSwiftとReactiveCocoaを比較する。
まずはFRPとは何かを簡単におさらいして、そのあと2つのフレームワークの細かい比較を見てもらう。読み終わる頃には、どちらのフレームワークが自分にあっているか判断ができるようになっているだろう。
リアクティブに行こう!
Functional Reactive Programmingとは何か?
Note: もしもう既にFRPのコンセプトをわかっているなら、次の"ReactiveCocoa vs RxSwift"セクションに進んで欲しい。
FRPは、Swiftがアナウンスされる前から既にオブジェクト指向プログラミングに対抗する方法論として人気を獲得していた。Haskellに始まりGo,JavaScriptなど、FRPの思想に影響を受けた実装が見つけられるだろう。なぜか?FRPの何が特別なのか?そしておそらく最も大事なのは、このパラダイムをSwiftでどう活用できるのか?
FRPはConal Elliottが提唱したプログラミングパラダイムである。彼の定義はとても具体的な意味論でここで見ることができる。雑に言うと、FRPは以下の2つのコンセプトの組み合わせである。
- Reactive Programming
- listenして、それに応じてアクションできるような非同期なデータストリームに着目。もっと学びたければこの素晴らしい説明を見て欲しい。
- Functional Programming
- 数学的な関数を用いた処理、不変性や表現性、そして変数や状態を減らすことに重きをおく。もっと学びたければSwift functional programming tutorialを見て欲しい。
Note: Andre StalzがオリジナルのFRPと実践的なアプローチについて彼の記事で説明している。"Why I cannot say FRP but i just did"
簡単な例
例を見るのが一番わかりやすい。ユーザの位置情報をトラックして彼女がコーヒーショップの近くに立ち寄った時にアラートしてくれるようなアプリを考えよう。
これをFRPの流儀に則ってプログラミングしようとすると
- それに応じてアクションできるような位置情報のストリームイベントを発するオブジェクトを作る。
- 発せられた位置情報イベントを見てコーヒーショップに近い位置情報があるかを探し、マッチするものをアラートする
ReactiveCocoaで書くと、こんな感じだ。
locationProducer // 1 .filter(ifLocationNearCoffeeShops) // 2 .startWithNext {[weak self] location in // 3 self?.alertUser(location) }
セクションごとに見ていこう。
locationProducer
は位置情報の変更を一定の周期で通知する。これをReactiveCocoaでは"signal"と呼び、RxSwiftでは"sequence"と呼ぶ。- 次に位置情報の更新に答えるために関数型プログラミングの手法を使う。
filter
メソッドはarrayのそれと同じように動き、それぞれの値をifLocationNearCoffeeShops
メソッドに渡す。もしtrue
が返されれば、そのイベントは次のステップの処理に進むことができる。 - 最後に、
startWithNext
はイベントが届いた時に実行されるクロージャを設定する。
上のコードは配列の値を変換するようなときに使われるものとよく似ている。しかし、これは少しだけ賢い…。このコードは非同期に動くのだ。filter関数とクロージャは"位置情報イベントの発生に応じて"実行される。
このシンタックスは少し妙だと感じるかもしれないが、このコードの根底にある意図はとてもわかりやすいだろう。これが関数型プログラミングの美しさであり、 values over time(宣言的で、どのようにではなく何が起きたかを示す)というコンセプトによくフィットするところである。
Note: もしもReactiveCocoaのシンタックスについて学びたいなら、私がGitHubに公開した例を見て欲しい。
イベントの変換
位置情報の例では、コーヒーショップの近くかどうかを見るためにフィルタリングする以外は何もせずにただストリームの受信を開始しただけであった。
もう一つのFRPの基本的な部分は、イベントを意味のある情報に組み合わせたり変換したりすることである。そのために、高階関数を用いる。
ご想像の通り、Swift functional programming tutorialの中にあった、map
,filter
,reduce
,combine
,そしてzip
などだ。
それでは先程のサンプルを、繰り返されたイベントをスキップし、近づいている位置情報をユーザフレンドリーなメッセージに変換するように変更してみよう。
locationProducer .skipRepeats() // 1 .filter(ifLocationNearCoffeeShops) .map(toHumanReadableLocation) // 2 .startWithNext {[weak self] readableLocation in self?.alertUser(readableLocation) }
追加された2行を見てみると
locationProducer
から発されたsignalにskipRepeats
オペレータを適用した。これは配列のようなものを持たないオペレーションで、ReactiveCocoa特有のものである。この関数は繰り返しになっている値を弾くだけのフィルターである。filter
関数が動作した後、イベントデータを他の型に変換するmap
が使われる。ここではCLLocation
からString
に変換している。
次は、これらFRPの旨味を知るためにはじめるべき
- シンプルなのに、パワフル
- 宣言的アプローチによってコードはよりわかりやすくなる
- 複雑なフローも管理や表示が簡単になる
ReactiveCocoa vs RxSwift
さて、FRPとはどういうもので、それがどのように複雑な非同期処理を簡潔にしてくれるか少しは分かったところで、2つの最も人気のあるFRPフレームワーク、ReactiveCocoaとRxSwiftについて、そしてどちらを選ぶのかを考えてみよう。
詳細に行く前に、それぞれのフレームワークの歴史を簡単に振り返ってみよう。
ReactiveCocoa
ReactiveCocoaはGitHubで生まれた。GitHubのMac版クライアントを作る中で、開発者はアプリケーションのデータフローに苦労していた。彼らはMicrosoftのCE向けライブラリReactiveExtensionsに影響され、Objective-C向けの実装を作り始めた。
チームがObjective-C版のv3.0のリリースに向けて進んでいた時に、Swiftがアナウンスされた。彼らはSwiftの関数的な部分はReactiveCocoaにとってとても補完的だと感じ、後にv3.0としてリリースされるSwift版の実装にすぐとりかかった。v3.0のシンタックスはとても関数的で、カリー化とパイプライン演算子を利用していた。
Swift2.0でprotocol-oriented programmingが提唱され、ReactiveCocoaに重要なAPI変更をもたらした。v4.0ではパイプ来年残滓が廃止され代わりにプロトコル拡張が使われる。
ReactiveCocoaは執筆時点でGitHubのスターが13000以上集まっている人気のライブラリである。
RxSwift
MicrosoftのReactiveExtensionsに影響されてJavaScript、Java、Scala、その他多くの言語にFRPの思想をもったフレームワークが生まれた。これらはFRPの実装に一般的なAPIを策定したReactiveXによって主導されており、これにより様々なフレームワークの作者がともに作業できるようになった。結果的に、RxScalaを使う開発者はRxJavaにも親しみやすくなった。
RxSwiftは最近ReactiveXファミリーに追加されたため、ReactiveCocoaのStar数には及ばない(執筆時点で4000)。しかしながら、RxSwiftはReactiveXの一部であり、今後の人気と長命さは約束されていると言えよう。
ここで面白いのは、RxSwiftもReactiveCocoaもともにReactiveExtensionsを祖先に持つということである!
RxSwift vs ReactiveCocoa
続いて詳細を見てみよう。RxSwiftとReactiveCocoaはFRPの実現の仕方に多少の違いがある。そのうちの幾つかを見てみよう。
Hot vs Cold Signals
通信のためのリクエストを作り、レスポンスをパースし、ユーザに見せるようなシーンを想像してみよう。
let requestFlow = networkRequest.flatMap(parseResponse) requestFlow.startWithNext {[weak self] result in self?.showResult(result) }
リクエストはsignalを購読した時に初期化される(startWithNext
したとき)。これらのsignalは、購読するまで"凍結された"状態なので、 cold と呼ばれる。
もう一つは hot なsignal。hot signalを購読した時、それは既に動作を開始しているかもしれないので、受け取ったイベントは3番目、4番目のイベントかもしれない。ぴったりな例を挙げると、キーボードのタップがそうである。キーボードのタップが「開始」するというのは妙だ。
おさらい:
- cold signal は購読して初めて動き始める。購読者ごとに動作が開始される。
requestFlow
を3回購読するということは、3つのリクエストを発行することを意味する。 - hot signal はもう既にイベントを送り始めてるかもしれない。新しく購読者が増えても、新しく開始されることはない。ふつうUIイベントはhot signalである。
ReactiveCocoaはhotとcoldそれぞれにSignal<T, E>
とSignalProducer<T, E>
という型を提供している。一方RxSwiftはObservable<T>
という型一つで両方を表現する。
hotとcoldの型が分かれているのといないのとで何が違うのか?
個人的には、signalの意味を知っているということは大事だと思っている。なぜなら特定のコンテキストでどのように動作するのかを知っておいたほうが良いからである。より複雑なシステムを作るときに、これは大きな違いとなる。
それぞれの型があるかどうかは関係なく、hotとcoldについて知っておくことは非常に大事である。
仮にhot signalを扱っているつもりでそれが実はcold signalだったとき、新たな購読者ごとに副作用が発生する。これはアプリケーションに甚大な影響を与えるだろう。よくある例だと、3,4個のエンティティがネットワークのリクエストをobserveしたがっているとき、それぞれの購読ごとに異なるリクエストが実行されるべきである。
ReactiveCocoaに1ポイント!
エラーハンドリング
エラーハンドリングの話をする前に、RxSwiftとReactiveCocoaにディスパッチされるイベントについて簡単に振り返ってみよう。両方とも、主に以下の3つのイベントを発行する。
Next<T>
- これは
T
型の新しい値がストリームにプッシュされるたびに発行される。位置情報の例だと、T
はCLLocation
である。
- これは
Completed
- イベントストリームの処理が完了したことを示す。これ以降に
Next<T>
やError<E>
が発生することはない。
- イベントストリームの処理が完了したことを示す。これ以降に
Error<E>
- エラーを示す。ネットワークリクエストの例だと、サーバーがエラーを返した時などに発生しうる。
E
はErrorType
に適合する型。これ以降にNext<T>
やCompleted
が発生することはない。
- エラーを示す。ネットワークリクエストの例だと、サーバーがエラーを返した時などに発生しうる。
お気づきかも知れないが、RxSwiftのObservable<T>
は持っていないがReactiveCocoaのSignal<T, E>
とSignalProducer<T, E>
は2つの型パラメータを持っている。E
という型はErrorType
のエラーを示す。RxSwiftでは内部でErrorType
のエラーを生成している。
これは一体どういうことか?
実践面では、RxSwiftではエラーを発するやり方はいくつかあることを意味する。
create { observer in observer.onError(NSError.init(domain: "NetworkServer", code: 1, userInfo: nil)) }
上のコードはsignal(RxSwiftではobservable)を作成して、すぐにエラーを発生させている。
あるいはこうも書ける。
create { observer in
observer.onError(MyDomainSpecificError.NetworkServer)
}
Observable
は「エラーはErrorTypeであればいい」ので、送りたいものを送ることができる。しかしそれは以下の様なケースですこし厄介になる。
enum MyDomanSpecificError: ErrorType { case NetworkServer case Parser case Persistence } func handleError(error: MyDomanSpecificError) { // Show alert with the error } observable.subscribeError {[weak self] error in self?.handleError(error) }
これは動かない。なぜならhandleError
が引数に取るのはMyDomainSpecificError
であり、ErrorType
ではない。あなたは次のことをしなければならない。
error
をMyDomainSpecificError
にキャストするerror
をキャストできなかったときのケースをハンドルする
1つめはas?
を使えば簡単に対応できるが、2つめは少し面倒だ。見込みのある解決策はUnknown
というcaseを追加することである。
enum MyDomanSpecificError: ErrorType { case NetworkServer case Parser case Persistence case Unknown } observable.subscribeError {[weak self] error in self?.handleError(error as? MyDomanSpecificError ?? .Unknown) }
ReactiveCocoaにおいては、Signal<T, E>
やSignalProducer<T, E>
を作った時、違う型のエラーを投げようとした時にコンパイラは文句をいえる。
ReactiveCocoaにもう1ポイント!
UIバインディング
UIKitのようなiOSの標準APIは、FRPの言葉をしゃべれない。もしもRxSwiftもReactiveCocoaも使わないならば、自分でAPIをブリッジしなければならない。例えば、tapのtarget actionをsignalやobservableに変換しなければならない。
ご想像の通り、多くの手間がかかる。だからReactiveCocoaとRxSwiftの両方はそれぞれ幾つかのブリッジとバインディングを提供している。
ReactiveCocoaにはObjective-Cの時代からの資産がある。Swiftにブリッジされたすでにある多くの資産も見つけることができる。これらの中にはUIバインドが入っているが、その他のいくつかの演算子はまだSwiftに書き換えられていない。もしObjective-Cで出来たクラスを使っているなら、Swiftの型に変換しなければならない(toSignalProducer
のようなメソッドも用意されている)。
それだけでなく、ドキュメントを読むよりもソースを読むほうが時間がかかると感じる。これは知っておくべきことだが、理論や考え方のドキュメントは夫妻となっている。使い方の部分のドキュメントはしっかりしてるけどね。
これを補うために、数十のチュートリアルが用意されている。
一方、RxSwiftのバインディングは使っていて楽しい!大きなカタログが有るだけでなく、山ほどの見本、完全なドキュメントがある。これだけでReactiveCocoaでなくRxSwiftを選択する理由になる人もいるだろう。
RxSwiftに1ポイント!
コミュニティ
ReactiveCocoaはRxSwiftよりもその歴史が長い。多数の開発者がいて、十分な量のチュートリアルがオンライン上にあり、SOのReactiveCocoaタグも非常に助けになるだろう。
ReactiveCocoaはSlackのチームを持っているが、209人しかいない小さなチームであり、多くの質問は回答されないままになってしまう。緊急の場合は、コアメンバーに直接メッセージを送らざるを得なかったし、おそらく他の人々もそうしているだろう。とはいえ、ほとんどの問題はオンライン上のチュートリアルで解決できるはずだ。
RxSwiftは新しくて、ほぼ一人の手で作り上げられている。Slackのチームもあり、961人のメンバーがいて、議論も活発である。疑問に答えてくれる人をいつでも見つけることができる。
全体的に、今のところいずれのコミュニティも異なる面で素晴らしく、ここでは引き分けとする。
どっちを選ぶべきなの?
Ash FurrowがReactiveCocoa vs RxSwiftで言うところによると
聞いてくれ、もしあなたが初心者なら問題ない。もちろん、技術的な違いはあるが、いずれも初心者にはとっつきづらい。まずは片方試してみて、そのあともう片方を試してみて欲しい。そして自分自身に問うてみると良い!そうすればなぜそれを選ぶのか理解できるだろう。
私も同じアドバイスをするだろう。
しかしながら、もしあなたがどちらかを選ばなければならなくて、かつ両方を試す時間が無い時のために、ここに私の提案を書いておく。
ReactiveCocoaを選ぶべきシーン
- システムをよりわかりやすく記述したい時。hotとcoldなsignalを型で区別すること、エラーの型をパラメータ化することは、システムをより良くするだろう。
- 実績があり、多くの人に使われ、多くのプロジェクトで使われたフレームワークを使いたい時。
RxSwiftを選ぶべきシーン
- UIバインディングがプロジェクトにとって重要なとき
- FRP初心者で手厚い支援が必要なとき
- RxJSもしくはRxJavaの経験があるとき。RxSwiftはReactiveXのorganization下にあるため、一つを知れば、他を知るのも容易である。
次はどこに行くべきか?
RxSwiftかReactiveCocoaか、どちらを選ぼうとももう後悔はしないだろう。いずれもシステムをよりわかりやすく記述するには素晴らしいフレームワークだ。
これもまた重要な事だが、一度RxSwiftやReactiveCocoaを知ると、互いを行き来するのはとても時間がかかるだろう。私の経験によると、ReactiveCocoaからRxSwiftに勉強のために移行した際は、エラーハンドリングが最もはまった場所だった。まあ、最も大きなマインドシフトはFRPそのものに慣れることで特定の実装は関係ないんだが。
以下のリンクは君のFRP、RxSwift、ReactiveCocoaとの旅の助けになるだろう。
- Conal Elliottのブログ
- Conal ElliottのStackOverflowの素晴らしい回答 What is (functional) reactive programming?
- Andre Staltzの必ず読むべき記事 Why I cannot say FRP but I just did
- RxSwiftのリポジトリ
- ReactiveCocoaのリポジトリ
- Rexのリポジトリ
- iOSエンジニアにとって究極のFRPの宝の埋蔵物。RxSwiftとReactiveCocoa両方のリソースがある。
- Marin TodorovによるRxSwift exploration
私はあなた達がこれらの素晴らしいライブラリの一つを将来のプロジェクトで使ってくれることを願っている。