読者です 読者をやめる 読者になる 読者になる

RxSwift つかってる部分の引き継ぎしてる

前回 に続き、担当してたアプリについて考えていたこと書き記してる。

とくに RxSwift がなぜ良いのか? ということは、説明がすこしむつかしいので、すこし丁寧に書くチャレンジをしてみている。冒頭のところ公開してみる。

--

Rxは何を解決できるか?

クライアントサイドのアプリケーションへの入力は、タイミングがまったく予測できず、おまけに、並列であちらこちらに到着します

イメージ図)

入力の種類 \ 時間 0.166秒 0.33秒 0.5秒 0.66秒
 ひとさし指の動き タップ! スワイプ開始!
 おにいさん指の動き タップ!
 HTTPレスポンス その1 到着!
 JSONのデコード 完了!
 写真の読み込み 完了!
 HTTPレスポンス その2 到着!
 PNGのデコード 完了!

これらバラバラのタイミングで連続して発火する入力を漏らさず受け取り、整合性を取っていかなければいけない、この辺りにクライアントサイドの(設計面での)難しさがあります。

iOS は、アプリが起動してから落ちるまで、1秒に60回くらいのハイペースで休まず画面を描きかえ続けています。

f:id:hadashia:20160825154943p:plain

(1秒に60フレーム)

そのため、我々アプリ層のプログラマは、画面に出力すべき内容の変更を検出し次第、即、それを反映させる責任があります。画面は、プログラムの処理が終わるのを一切待ってはくれず、こうしている間にも、随時、描き直されまくられているのです。

まとめると、入力は並行してバラバラにくるし、出力は即反映させないといけない、ということです。 これは、入力 → (処理) → 出力、というごく単純なシーケンスを考えるだけでは、全体を捉えることがむつかしいです。なにかすごいパラダイムの助けを借りて、人間に理解できる脳内モデルを導入したいところ。

ところで、なぜ入力は、並行してやってくるのでしょうか? それは、iOSがしている1秒に60回の画面描き換えをブロックしてはいけないからです。

UIを書き換えているスレッドは、1/60s = 1.66ms 以内に画面を書き換え終わらなければいけないため、このスレッドで時間のかかる処理はできません。I/O待ちは当然非同期、CPUバウンドな処理は別スレッド、周りを見渡せば、アプリ層そのものが非同期だらけです。アプリ層そのものが複雑なのです。

ご存知のとおり、非同期処理が多くなると、上から下に順番に処理を書いているぶんには簡単だったことも、途端に難しくなります。

たとえば、

  • 複数の入力データをくみあわせて、ひとつの出力をはじきだす
  • エラーがどこで発生しようが、いつも同じ位置で処理する
  • 処理と処理の順番を指定する
  • 不要になった処理を途中で止める

もちろん、どれもこれも、同期的に上から下に処理が流れるだけであれば単純なことなのですが、非同期がからむと途端に難しくなります。それに、iOS はマルチスレッドの実行ができるから、並列処理特有の問題もあります。変数のデータ競合を避けるとか。

これがクライアントサイドの設計の本質的に難解な点です。ドメインロジックとプレゼンテーションレイヤを分離するとか、UIとデータをバインドするとかでは、秩序を齎すに十分ではないことがわかるとおもいます。

このアプリでは これに対応するため Rx をつかいました。じつを言うと、Rx なら、上の問題を解決することが簡単にできます。

  • 複数の入力データをくみあわせて、ひとつの出力をはじきだす → Observableを合成するだけ
  • すべてのエラーを、適切なタイミングで出す → onError にエラーが伝搬してくる
  • 複数の処理を適切に待ち合わせる → スケジューラを使えば解決
  • 不要になった処理のキャンセル → Disposableを定義してけば自動でキャンセルされる
  • race condition を避ける → イミュータブルなデータと副作用のない関数の操作がメインなので問題になりにくい。必要なときは、スケジューラでスレッドを指定する

このリストをみれば、Rx と、Rxの亜種との違いがわかると思います。 Rxは、言語仕様によるサポートを一切必要とせず、それどころかどんな言語にもまったく同じ超強力かつ型安全なインターフェイスを提供することに成功しているのですが、なかでも、Disposable 、スケジューラ などの実用的な周辺機能があるおかげで、非同期の難しさに立ち向かうことができます。これらは、みんなの力で言語をまたいで移植され、いきいきと活動し続けています。

変更可能性(ミュータビリティ)

複数の非同期処理 の整合性をとる、これを素朴に実装すると、おびただしい数の状態変数が生まれることになります。

非同期処理によって、あちらこちらに点在するコールバック関数が呼ばれるわけですが、これらを連携させるには、コールバック関数の外側のスコープに変数をつくって、読み書きするしかないからです。

だけど考えてみてください。これは、原理的にやってることがグローバル変数と一緒です。どこからどの変数が更新されるかを保証することも仮定することもできないので、デバッグが非常に困難です。

もしも、変数の内容がどこかで書き換わることがなく、関数に副作用がないとしたら、状況によって挙動が変わるということがなくなるので、とても見通しがよくなります。

リアクティブプログラミングは、そういった考え方をもたらしてくれます。中間の状態を変数で管理するという必要がなくなるのです。 イメージは、事前にたくさんのイベント同士の関連をすべて一箇所で宣言する、それだけです。 イベントとイベントの関係を事前に宣言しておけるので、たくさんのイベント間で1つの変数を読み書きする必要がありません。

書き換わることがない変数には、いくつか優れている点があります * マルチスレッド間でデータ競合を引き起こすことがありえない。安心して別スレッドへ渡せる * 参照を共有する必要がないので、データは値型でよくなる。すると参照のコピーが走らなくなるので、Swift の ARC によってリファレンスカウントを管理する必要がなくなる。余計なバイトコードが生成されなくなるし、参照を数える必要がなくなり、パフォーマンスがよい。(コピーの時間が無視できないほどの巨大なデータはこのかぎりではない)

GUI プログラミングで、 関数型言語のムーブメントが盛り上がっているのは、このような複雑な状態管理を解決できるアプローチになりうるからです。

関数の副作用を廃して、始点の入力さえあれば、中間のデータを保持することをかんがえなくてもすべてを宣言できる、この考え方は、非同期だらけのなにかを書くのに非常にマッチします。Swift が関数型の影響を受けているのは、この目的と無関係ではないでしょう。

サーバサイドプログラミングとの違い

結果が変わり次第、随時出力を求められるというのは、サーバサイドにはあまり見られない制約です。

HTTPなサーバサイドのプログラミングを考えると、アプリ層への入力は、「リクエスト」という単一の入力だけがあり、相手に対して応答するのも一度だけ。しかもその間、相手を待たせておくことができます。

入力 → (何か処理) → 出力

サーバサイドは、アプリケーション層自体よりも、むしろより低レイヤなDB層の設計のほうが大変だったり、アプリ層そのものでなく、アプリ自体を並列で並べるアーキテチャ面などに難しさがある傾向があります。

ネイティブアプリケーションは、オーソドックスなWebページとは根本的に難しさが違います。アプリ層の中そのものが非同期だらけです。そのため、クライアントサイドではRxなど、アプリ層で別のパラダイムを導入するムーブメントがあるます。

Rx 入門