【わかりやすい】RxJSで始める関数リアクティブ・プログラミング
0.前置き
本投稿は関数リアクティブプログラミングの入門記事です。関数リアクティブプログラミングの概念、その目指しているものを伝えるにはある程度、分量が必要でした。私は数学者ではないですので、技術の一利用者としてできるだけ分かりやすく書きました。少し長いですが、お付き合いいただければ幸いです。
今、リアクティブ・プログラミングがブームです。ただリアクティブ・プログラミングを使うには考え方を学ばなくてはいけません。また、向き不向きも学ばなくてはなりません。関数リアクティブ・プログラミングを使うと時にはソースコードが長くなることもあります。ごくごく簡単なプログラミングについて、私もこちらの記事と同じようにリアクティブ・プログラミングまではいらないと私も思っています。
しかし、世の中Aをする、Bをするといった簡単なプログラムばかりではありません。AをしていたらBをしない、何か終わるまで待ちたい、条件が複雑に絡まるようになっていきます。プログラムの条件が複雑なってくると、関数リアクティブ・プログラミングは実力を発揮し始めます。
複雑な条件を記述しようとすると、ソースコードはスパゲッティのようになって、最後は副作用が生じるといったパターンになりがちですが、リアクティブ・プログラミングを使うことにより、複雑なイベント処理を伴うプログラムを高い品質で作成できます。
以下では関数リアクティブ・プログラミングのライブラリとして有名なRxJSを使って、関数リアクティブ・プログラミングをはじめてみましょう。RxJSには他の言語にも同様のRx**シリーズがありますので、ぜひ、皆さんのお好きな言語でリアクティブ・プログラミングを始めてください。
1.本稿の準備(RxJSのインストール)
RxJSはJavaScriptのライブラリです。もちろんブラウザ等でも動作しますが、簡単に確認できるよう、本稿ではコマンドラインで実行することにします。コマンドラインでJavaScriptを実行するために、まずはnode.jsをインストールする必要があります。以下のLIGさんの記事などをご参考いただきインストールしてください。node.jsがインストールできたら、RxJSをインストールしておきます。
>npm install rx --save-dev
2.関数リアクティブ・プログラミングを試してみよう。
関数リアクティブ・プログラミングではイベントをストリームで扱います。ストリームとはイベントの発生源と受け取り先の関係を示す流れのことです。イベントが起きる前にストリームを作成して、イベントを処理する流れを決めておき、その流れに従って処理します。
従来のプログラムではイベントが起きてから、その内容を判断して、フラグを立てて、どう処理をするか決めるといったことやりがちです。それが積み重なって、プログラムはどんどん複雑になります。そう言った事後で処理をするのではなく、関数リアクティブプログラミングでは、事前に処理の一連の流れがどうなるか、ストリームで明確にし、ストリームで処理をすることが特徴です。
まずは最も簡単な例、配列に入っている要素ごとにイベントが起きるストリームを作成して、リアクティブ・プログラミングの様子をみてみましょう。
var Rx = require('rx');
//イベントの元となる配列
var array = [1,2,3,4,5,6,7,8,9,10];
//配列から配列の要素ごとに10回イベントが起きる発生源を作成する。
var source = Rx.Observable.fromArray(array);
//イベントを受け取るよう指定する(配列の要素を10回受け取る)
//これでイベントの処理の流れが決まった。
source.subscribe( function( x ){
console.log( x );
});
イベント元のsouceを作り、subscribeで受け取り元の振る舞いを決めました。これがイベントの流れ、ストリームです。このストリームでは下の図のようになっていることが理解できます。
まだまだ導入なのでとても単純ですが、こういったイベントの流れを作ることが関数リアクティブプログラミングのポイントだと覚えておいてください。
早速、array.jsを実行してみましょう。 10個の配列の要素がそれぞれイベントとなって、subscribeで指定した関数で処理されていることがわかります。
>node array.js
1
2
3
4
5
6
7
8
9
10
ここまでのまとめ。
* 関数リアクティブ・プログラミングではイベントを事前に決めた流れ=ストリームで処理する。
* ストリームのイベントの発生源をObservable(観測できるもの)、イベントの受け取り先をObserver(観測者)と言う。
* ストリームの流れ、つまり、イベント源とイベントを受け取る関係はObservableをObserverによりsubscribeすることで作る。
* Rx.Observable.fromArrayで配列をイベント源とするストリームを作成できる。
3.特定の条件のときだけ処理する(filter)
みなさんは3の倍数だけアホになるというギャグが流行しましたのを覚えているでしょうか。(もう10年近くも前になるのですね。。。)それにならって3の倍数だけ処理するという例を実現してみましょう。
3の倍数だけを取り出したい場合、先ほどのイベントの流れにフィルターをかけて、流れの中で無駄なイベント(3の倍数以外)を排除すれば良いです。では、先ほどの例でObserverがObservableをsubscribeする前にfilterを挟んでみましょう。
var Rx = require('rx');
var array = [1,2,3,4,5,6,7,8,9,10];
//配列から配列の要素ごとに10回イベントが起きるストリームを作成する。
var source = Rx.Observable.fromArray(array);
//イベントの発生源から3の倍数だけを取り出すフィルターを追加
//フィルターもまたイベント源
var filtered = source.filter( function( x ){
//3の倍数時だけOKを返却。
if( x % 3 == 0 ){
return true;
}
});
//filterされたイベント源からイベントを受け取るよう指定してストリームが完成
filtered.subscribe( function( x ){
console.log( x );
});
filterはあるObservableの指定された条件の時にイベントを起こすObservableを作成する関数です。これでストリームにfilterが追加されました。こちらを実行してみましょう。
>node filtered_array.js
3
6
9
見事に3の倍数だけが表示されるようになりました。各種イベントの発生源であるObservableを変換し、関心のあるイベントだけを抽出されたObservableを作り出す。これが関数リアクティブプログラミング(FRP)のキモになってきます。
このストリームに対して、従来であれば、多分、表示直前に3で割り切れるかどうかを確認する以下のようなコードを書いていたと思います。つまり、従来のプログラミングは条件と処理が非常に密接な関係にあります。
if( x % 3 == 0 ){
console.log( x );
}
問題が簡単な時は、この方法でも問題ないでしょう。ただ、それがどんどん複雑になった時を想像してみましょう。表示処理と条件が混在するということは、あちこちに似たような条件を持ったコードが潜んでいくということです。やがて副作用をもたらしてバグとなったり、あちらこちらで挙動が変わるのでプログラムの見通しが悪くなっていくことになるでしょう。
一方で関数リアクティブプログラミングに戻って考えると、今回はとても簡単な例でしたが、filterの3で割り切れる場合という条件と、それが起きた時の処理(console.logの表示処理)に分けて専念することができたと言えます。
どこで条件が埋め込まれるかわからない従来型のプログラムに対し、関数リアクティブプログラミングでは事前に条件をストリームで分離します。filterという簡単な例でしたが、関数リアクティブプログラミングが予期しない動作をすることを防ぐプログラムにつながることが感覚的にもお分かり頂けると思います。
ここまでのまとめ。
* filter関数でObservableを特定の条件のイベントのときだけイベントを発生させるObservableに変換できる。
* Observableを変換しながら、最終的に関心のあるイベントのみを受け取る条件を作るのがキモ。
4.イベントの種類(メッセージ)を変換する。(map)
先ほどまでずっとイベントが起きた際に渡されるものは数字でした。イベントの種類(メッセージ)を変換する場合はmapを使いましょう。mapはもとになったイベントの発生源のObservableから得たイベントの内容(メッセージ)を変換して、イベントを作るObservableを生成します。
var Rx = require( 'rx' );
var array = [1,2,3,4,5,6,7,8,9,10];
//配列から配列の要素ごとに10回イベントが起きるストリームを作成する。
var source = Rx.Observable.fromArray(array);
//イベントの発生源からの内容を変換するmapを追加
var mapped = source.map( function( x ){
var ret = ["-","いち","に","さん","よん","ご","ろく","なな","はち","きゅう","じゅう"];
return ret[x];
});
//filterされたストリームからイベントを受け取るよう指定する
mapped.subscribe( function( x ){
console.log( x );
});
ここまでのまとめ。
* map関数でイベントのメッセージを変換したObservableを作成できる。
5.テスト向けのイベント源を覚える
ここまでは一番簡単配列からのイベント源を作成しましたが、ここから先ではテスト用に色々なイベントが作成できるよう、少しイベント源の引き出しを準備しておきましょう。
オペレータ | 内容 |
---|---|
timer | 指定時間後に発火するObservableが作成できる |
interval | 指定時間後に繰り返し発火するObservableを作成できる |
range | 指定した範囲のイベントを発火するObservableが作成できる |
take | 指定された回数だけ発火するObservableが作成できる |
こちらのRx逆引きがよくまとめられていると思います。
6.イベント源を合わせる(merge)
それでは5秒後と7秒後に発火するストリームを作成してみましょう。
merge関数を使うことでイベント源を統合することができます。
var Rx = require( 'rx' );
var five = Rx.Observable.timer( 5000 ).map( "5秒" );
var seven = Rx.Observable.timer( 7000 ).map( "7秒" );
var merge = five.merge( seven );
merge.subscribe( function( x ){
console.log( x );
});
こちらを実行してみましょう。
>node time.js
5秒
7秒
5秒や7秒といった単体だけでなく、2つのイベントを組み合わせて発火することができることが分かりました。様々なイベント源を組み合わせてプログラミングができることがわかりますね。
7.さらに複雑な条件を作り上げる(and, thenDo, when)
ここまでmergeやfilterいった少しだけ条件が入ったイベントを制御してみました。ここでは更に高度にやってみましょう。AとBが起きたらなど複雑な条件を記述するために、and/then/whenというやり方を覚えていきましょう。ここは今までのRxと少し異なった記述方法が必要になります。
今までのmergeやfilterはObservable(シーケンス)を直接リターンしてくれました。andはこれらと違って、Patternをリターンしてきます。なので、今まで通りの扱いを行うことはできません。少し注意が必要です。
Patternができたら、次にthenDo(言語によってはthen)で Planを作ります。PlanはPatternが起きた時に、それらの条件をどのようなメッセージのイベントにするかという「計画」です。
そして、Planができたら最後にwhenでObservable(シーケンス)を取得することができます。このようにPatternやPlanを駆使することにより、複雑な条件もイベントストリームの形で取り扱うことができます。
それをまとめると、下のようになります。
複雑な条件の手順
順番 | 使うもの | 意味 |
---|---|---|
1 | and | Observableから、どんな時かを意味するPatternを作成する |
2 | thenDo (言語によっては then) | Patternが起きた時に発生させるメッセージを作るPlanを作成する |
3 | when | Planをシーケンス(Observable)にする |
この事を理解しておいて、次のReactiveX本家にある図を見てみましょう。
一番上がストリームで丸や五角形や星形がやってくる形のストリームで、その下が色がやってくるストリームです。その2つからandによってPatternを作成します。Patternから色つきの形イベントが起きるようPlanを作り、最後はWhenでストリームにするといった内容が読み取れましたね?
これで、複雑な条件を作り出すことがあなたにもできる素地ができたはずです。早速、3つのものが揃った時に発火するイベントストリームを作成してみましょう。以下では1秒ごとに5回、2秒ごとに5回、3秒ごとに5回、発火するストリームを3種類用意し、イベント源の3種類が全部揃った時にログをするストリームを作成してみます。
var Rx = require('rx');
var observableA = Rx.Observable.interval( 1000 ).take(5);
var observableB = Rx.Observable.interval( 2000 ).take(5);
var observableC = Rx.Observable.interval( 3000 ).take(5);
// 複雑な条件をpatternにします。
var pattern = observableA.and( observableB );
// thenでpatternが起きたらどうするかのplanを作ります。
var plan = pattern.thenDo(function( x , y, z ){
return "3つ揃ったよ!";
});
//whenでシーケンスを取り出そう。
var complexCondition = Rx.Observable.when(plan);
complexCondition.subscribe( function( x ){
console.log( x );
});
出力は以下のようになります。
3つ揃ったよ!
3つ揃ったよ!
3つ揃ったよ!
3つ揃ったよ!
3つ揃ったよ!
8.エラーの扱い
ここまでエラーの取り扱いを行なってきませんでしたが、エラーはイベントストリームを流れる幅流として扱うことができます。これまではsubscribeに、ずっとconsole.logで表示する、関数を1つ渡してきました。実はこのsubscribeには3つの関数を渡すことができます。エラー時の関数とストリームがすべて終わった際の関数です。
var observable = .....
observable.subscribe(
// 正常時
function( x ){
...
},
// エラー時
function( y ){
...
},
//完了時
function (){
...
}
);
9.関数リアクティブ・プログラミングと一方向性、仮想DOM
ここまでに関数リアクティブ・プログラミングの概要を描きました。RxJSの使い方も勉強しました。関数リアクティブ・プログラミングがどのようなものか見えてきたのではないかと思います。最後に関数リアクティブ・プログラミングが切り開いていく将来像について触れておきたいと思います。
関数リアクティブ・プログラミングでは、イベントの発生源からどのように関心のあるイベントを取り出すかを取り扱うプログラミング技術でした。イベントが起きてからどう処理していくかを判断するのではなく、イベントのストリームにより事前に処理の流れを決めておくプログラミングだということを掴んでいただけたのではないかと思います。
以下ではこのことがプログラミングにどういった影響を及ぼすか考えてみましょう。例えば、AをしてからBを待ってそれからCをして・・・・と様々な処理を行って画面の描画をするケースを考えてみましす。関数リアクティブ・プログラミングを使うと、先ほどまでの説明の通り、「Aをして・・・」のような条件の部分はリアクティブ・プログラミングに任せ、様々な処理ができたということを前提に最後の画面の描画をする処理だけを分離することができます。
関数リアクティブ・プログラミングではユーザー操作などの様々な条件をイベントのストリームで処理し、最後に表示をするだけの処理を行います。流れの向きは常に一定の向きです。関数リアクティブ・プログラミングはプログラム全体に一方向(unidirectional)の流れを生み出します。
一方向性が実現できると、後は描画すれば良いなど特定の処理に専念できます。その例の一つが仮想DOMという考え方です。
ご存知の通り、HTMLはツリー構造をしています。ユーザーに操作されてHTMLを変更する場合、そのツリーの一部を変更するだけで良く、大きなツリーをごっそり作り直す必要はありません。変更があった部分のほんの少しのエレメントだけを書き換えれば良いのですが、Webを部品化している以上、ある要素以下の大きなブロック全体をレンダリングし直すといったケースが多いのではないでしょうか。
そこでHTML構造をコピーした仮想のDOMによってツリーの変更を管理し、本当に変更が必要な部分だけ、実際のHTMLツリーに反映するといった仮想DOMの考え方が出てきます。仮想DOMにより、大幅にレンダリング時間は短縮されます。
複雑な条件処理をハンドリングする関数リアクティブ・プログラミングと描画に特化した仮想DOM、これは非常に相性の良い組み合わせとになります。それをよりしっかりとした仕組みで作り上げたReact/Fluxなどは、まさに慧眼と言えましょう。
一方向的な処理は、これからあらゆる言語やフレームワークで意識される概念になるでしょう。また、ひょっとすると以前の投稿で可能性を示唆したように、非常に長い目で見た時にはビジネスコンポーネント内で隠蔽されていくかもしれません。
これから長い目で見て、関数リアクティブ・プログラミングがソフトウェア設計のパラダイムが少しずつ変えていくのを楽しみ追いかけていきましょう。