Rx(JS)に入門する前に知っておきたいN個のこと
アドベントカレンダードリブンダイエットに失敗中の小飼です。
走っているのに中々痩せません。どうなってるんだ。。。
※この記事はRxJS Advent Calendar 2015の第二十日目の記事です。
概要
最近チラホラRxJS
についての記事を見るようになってきました。
非同期処理を簡潔に書けることについてのポジティブなポストを見かける一方で、主に学習コストの面でネガティブな意見が目立っていることも否定できません。
私は業務でバリバリRxJS
を使いこなしている!というようなレベルでは到底ありませんが、入門ブログ・書籍などを読んだり簡単なコードを書いてみて、事前に〇〇について知っていると理解が早そう
と思った点がいくつかありました。
そこで本稿ではRx(JS)に入門する前に知っておきたいN個のこと
と題して、事前に知っておくとスムーズにRxJS
に入門出来るんじゃないかなと思うことを書いていきます。
と言ってもご紹介したいのは基本的なデザインパターンについての事なので、既にご存知の方も多いと思います。
ただ、そういった方でも事前にこのデザインパターンに強く関連があるのだ
と前もって心積もりをしておくことで、スムーズな理解が得られるかも知れません。
また、説明を簡単にするために、諸々を単純化し過ぎている箇所があるかも知れません。
そこはひとえに私の能力不足によるところですので、ガチな方々にはそこら辺をご容赦お願いします :bow:
前提: 非同期処理は難しい
JavaScript
によるアプリケーションを作成するにあたって、非同期処理というのは避けて通れません。
これは、いつ発生するかわからないユーザーからの入力をevent loop
で捌いていくJavaScript
の言語設計の宿命と言えます。
この非同期処理の操作というのは一般にわかりづらいものになりがちです。
いつ完了するのかが予め分かりませんし、複数の非同期処理がある場合にも完了順序は保証されません。
また、実アプリケーション上ではしばしば完了しない可能性もあります。
非同期処理ってどういうこと?JavaScriptで一から学ぶ
そこでJavaScript
界隈では、この非同期処理を分かりやすく記述するための様々な技法が取り入れられてきました。
例えばPromise
やGenerator
を使った同期処理風な記述方法などです。
ただ、実アプリケーションではしばしば複雑な非同期処理の組み合わせを実装する必要に迫られます。
例えばユーザーのキーボード入力に特定の文字列を検知したタイミングでサーバーにリクエストを発行したかったり(rx
, cycle
などのnpm
ライブラリに存在する単語が入力されたら、該当しそうなライブラリの情報をサーバーから取ってくる、みたいな)、サーバーへのリクエストを複数回行ってみた内の特定のレスポンスをを使用したい(ジョークAPIに?random=true
のようなクエリ付きでリクエストを投げた時に、Chuck Norris
という文字列を含まないレスポンスだけを採用したい)というようなケースです。
もちろんPromise
やGenerator
のような慣れ親しんだ概念でも充分に実装可能ですが、往々にして不安定で複雑な実装になってしまいがちです。
そんな非同期処理の組み合わせを、もっとシンプルで安定した実装にするためのアイディアがReactive Extensions(ReactiveXまたはRx)
です
Reactive Extensionsとは?
公式サイトのトップには、
An API for asynchronous programming with observable streams
オブザーバブルストリームによる、非同期プログラミングのためのAPI
とあります。
これだけではよく分かりません。。。
更に下にスクロールすると、
ReactiveX is a combination of the best ideas from the Observer pattern, the Iterator pattern, and functional programming
ReactiveXはオブザーバパターンとイテレータパターン、それから関数型プログラミングの良いアイディアを組み合わせたものです
とあります。
オブザーバパターンとイテレータパターン(これを指してオブザーバブルストリームと呼んでいるようです)に関数型プログラミングのエッセンスが組み合わさると、
なぜ非同期プログラミングの記述が簡単になるのでしょうか?
そこを納得できれば、難しそうなRx
に入門するメリットが見えてきそうに思います。
Rx
に入門する前に・その1: 反復する要素の操作は簡単
例えば配列の操作というのは普段からよく記述していて、特に引っ掛かるところはないかと思います。
配列の内容を一部変換したり、
['I', 'like', 'meat.'].map((str)=> str === 'meat' ? 'vegetable.' : str)
// ['i', 'like', 'vegetable.']
フィルタリングしたりするような操作は日常的に行っていることです。
['I', 'like', 'meat.'].filter((str)=> str.match(/./))
// ['meat.']
操作対象のデータが配列に格納されていなくとも、イテレータパターンを用いて任意のデータを反復処理しやすい形に変換してあげれば、同じように簡単な操作が可能になります。
const iterable = {
someData: [ 'I', 'like', 'meat' ],
hasNext() {
return this.someData.length > 0;
},
next() {
this.someData.shift();
}
}
while(iterable.hasNext()) {
console.log(iterable.someData);
iterable.next();
}
/*
[ 'I', 'like', 'meat' ]
[ 'like', 'meat' ]
[ 'meat' ]
*/
Rx
に入門する前に・その2: 非同期処理の監視は簡単
特にJavaScript
界隈ではオブザーバパターンによる実装は至る所にあり、例えばブラウザにおけるクリックイベントの監視などは頻繁に行っているはずですので、こちらも容易に操作出来ると思います。
window.addEventListener('click', function(ev) {
console.log(ev);
// MouseEvent {isTrusted: true ...}
})
Rx
に入門する前に・その3: 非同期なイベントを反復する要素と捉える
もしイテレータパターンとオブザーバパターンを組み合わせて、非同期なイベントを反復処理できるような何か
にすることが出来れば、非同期なイベントを複雑に組み合わせたり変換したりしても、理解のしやすさを保ったコードを記述することが出来るかも知れません。
別の言い方をすると、非同期なイベントを時間軸に沿ったイベントの配列のようなイメージで捉えることで、理解のしやすいコードを書けるかも知れない、というような感じです。
こういった、配列のように操作できる監視可能な非同期イベントの集合を指して、Rx
ではObservable Stream
と呼んでいるようです。
個人的な解釈ですが、ここで説明したObservable Stream
はNode.js
で出てくるStream
と同じものとして捉えて良いと思います。
ただしRx
にはObservable Stream ≒ Stream
を配列っぽく便利に操作するための大量のAPI
群が付随しているという違いがあります。
Stream
という、ブラウザJavaScript
界隈ではまだ馴染みの薄いように思われる概念の理解と、そのStream
を操作するAPI群
への理解を混同しないことが、Rx
に入門する際に大事になってくるんじゃないかと思っています。
Rx(Observable Stream) |
Node.js(Stream) |
|
---|---|---|
Streamへの変換 | ◯ | ◯ |
Streamの観測 | ◯ | ◯ |
underscore/lodash 的な便利なAPI群 |
◯ | x |
※この説明では関数型プログラミング
の部分について触れていませんが、Rxへの入門の準備
という目的をややこしくしてしまうと思ったので省いています。
(私はObservable Stream
を外部状態に依存しない書き方で使うことで安定して綺麗なコードを書ける、みたいな理解の仕方をしています)
実装例
長々と個人的な解釈を書き連ねてきたので、この辺で実際のコードに落とし込んでみたいと思います。
ここではマークダウンエディタを実装している想定で、ユーザーのキーボード入力イベントから、`(逆クォート記号)で囲まれた文字列を
span
タグで囲んだhtml文字列
に変換するような処理を実装します。
ただし、変換したhtml文字列
をその度に描画していると描画コストが掛かり過ぎてしまうという想定で、
500ms
毎に最新のhtml文字列
を検出したいという仕様があるとしました。
準備
まず、ユーザーのキーボード入力を模したinput
イベントを発行するモジュールを作成します。
// event-mock.js
const EventEmitter = require('events').EventEmitter;
const eventListener = new EventEmitter();
const texts = 'ユーザーが打ち込んだマークダウン。
途中の`syntax-highlight`のように記述された箇所に、
シンタックスハイライトを`実装`する
';
function userInputMock(str) {
if (str.length === 0) {
return;
}
// ユーザーが`interval`変数の間隔でキー入力をしているようなイメージです
const interval = Math.random() * 50;
setTimeout(()=> {
eventListener.emit('input', str[0]);
userInputMock(str.substr(1));
}, interval);
}
userInputMock(texts);
module.exports = eventListener;
イベントリスナーによる実装
これを従来のオブザーバパターンによるイベントリスナーで単純に実装してみるとこんな感じになりました。
(例えばFlux
のActionCreators
層に実装したと読み代えて頂くこともできると思います)
const eventListener = require('./input-event');
var resultStr = '';
var appearedCount = 0;
var isEnd = false;
eventListener.on('input', (str)=> {
if (resultStr.length === 0){
resultStr += `<p>${str}`;
} else if (str === '
') {
isEnd = true;
resultStr += `</p>`;
} else if (str === '`') {
appearedCount++;
if (appearedCount % 2 !== 0) {
resultStr += `<span>`;
} else {
resultStr += `</span>`;
}
} else {
resultStr += str;
}
});
var interval = setInterval(()=> {
console.log(resultStr);
// <p>ユーザーが打ち込んだマークダウン。途中の<span>syntax-highlight</span>のように記述された箇所に、
// シンタックスハイライトを<span>実装</span>する</p>
if (isEnd) {
clearInterval(interval);
}
}, 500);
resultStr
やappearedCount
など、イベントリスナーの外部スコープに存在する変数を使った実装になっており、ちょっと不安な気持ちです。
if
文のネストが深いのも、やや気に入りません。少なくとも読みやすいコードには出来ていいないと思います。
(もちろん、もっと分かりやすいコードにするための工夫の余地はありますが、説明を単純にするためにわざと短絡的に実装しています)
配列に対する実装
Rx
を使えば、非同期イベントを配列のように処理できるはずですので、事前準備として単なる配列に対して同様の処理を実装してみます。
// ユーザーの入力イベントを以下のような文字の配列であると仮定します
const inputs = [
'ユ', 'ー', 'ザ', 'ー', 'が', '打', 'ち', '込', 'ん', 'だ', 'マ', 'ー', 'ク', 'ダ', 'ウ', 'ン', '。',
'途', '中', 'の', '`', 's', 'y', 'n', 't', 'a', 'x', '-', 'h', 'i', 'g', 'h', 'l', 'i', 'g', 'h', 't', '`',
'の', 'よ', 'う', 'に', '記', '述', 'さ', 'れ', 'た', '箇', '所', 'に', '、',
'シ', 'ン', 'タ', 'ッ', 'ク', 'ス', 'ハ', 'イ', 'ラ', 'イ', 'ト', 'を', '`', '実', '装', '`', 'す', 'る', '
'
];
// 処理したい内容に併せて、`inputs`配列をフィルタするための条件を生成する関数(Predicate関数)を作っておきます。
// 一文字目や最後の改行、`(逆クォート記号)をフィルタリングしたり、その逆をフィルタリングしたりできる関数を生成します。
var filterFirst = (isFirst)=> isFirst ? (str, i)=> i === 0 : (str, i)=> i !== 0;
var filterLast = (isLast)=> isLast ? (str)=> str === '
' : (str)=> str !== '
';
var filterSign = (isSign)=> isSign ? (str)=> str === '`' : (str)=> str !== '`';
// 上の3つの関数から、必要に応じて`inputs`配列を変換します
const first = inputs.filter(filterFirst(true)).map((val)=> `<p>${val}`);
const last = inputs.filter(filterLast(true)).map(()=> '</p>');
const sign = inputs.filter(filterSign(true)).map((val, i)=> i % 2 === 0 ? '<span>' : '</span>');
console.log(first); // ['<p>ユ']
console.log(last); // [ '</p>' ]
console.log(sign); // [ '<span>', '</span>', '<span>', '</span>' ]
// フィルタリングされなかった残りの文字を抽出します。
const strings = inputs
.filter(filterFirst(false))
.filter(filterLast(false))
.filter(filterSign(false));
console.log(strings);
/*
[
'ー', 'ザ', 'ー', 'が', '打', 'ち', '込', 'ん', 'だ', 'マ', 'ー', 'ク', 'ダ', 'ウ', 'ン', '。',
'途', '中', 'の', 's', 'y', 'n', 't', 'a', 'x', '-', 'h', 'i', 'g', 'h', 'l', 'i', 'g', 'h', 't',
'の', 'よ', 'う', 'に', '記', '述', 'さ', 'れ', 'た', '箇', '所', 'に', '、',
'シ', 'ン', 'タ', 'ッ', 'ク', 'ス', 'ハ', 'イ', 'ラ', 'イ', 'ト', 'を', '実', '装', 'す', 'る'
]
*/
配列のフィルタリングの際に、大本のinputs
配列におけるインデックスを失ってしまうので、出来上がった配列達を正しく合成することが出来ませんが、考え方としてはこういう感じになるかと思います。(ちゃんと実装するならinputs
配列を{ char: '字', index: 0 }のような要素の配列にしてあげる必要がありそうです)
Rxによる実装
上述の配列に対する実装を参考に、Rx
を用いたObservable Stream
に対する実装に変換してみたいと思います。
const Rx = require('rx');
const eventListener = require('./input-event');
const input$ = Rx.Observable.fromEvent(eventListener, 'input');
// フィルタ用のPredicate関数を返す
var filterFirst = (isFirst)=> isFirst ? (str, i)=> i === 0 : (str, i)=> i !== 0;
var filterLast = (isLast)=> isLast ? (str)=> str === '
' : (str)=> str !== '
';
var filterSign = (isSign)=> isSign ? (str)=> str === '`' : (str)=> str !== '`';
const first$ = input$.filter(filterFirst(true)).map((val)=> `<p>${val}`);
const last$ = input$.filter(filterLast(true)).map(()=> '</p>');
const sign$ = input$.filter(filterSign(true)).map((val, i)=> i % 2 === 0 ? '<span>' : '</span>');
const strings$ = input$
.filter(filterFirst(false))
.filter(filterLast(false))
.filter(filterSign(false));
// 時間軸に対する位置を、配列に対するインデックスとして捉えられるので、大本の`Observable Stream`の中でのオフセットを保ったまま
// 出来上がった`Observable Stream`を正しく合成することが出来ます
const merged$ = Rx.Observable.merge(strings$, first$, last$, sign$).scan((strings, str)=> strings + str);
const result$ = merged$.debounce(500);
result$.subscribe((val)=> {
console.log(val);
// <p>ユーザーが打ち込んだマークダウン。途中の<span>syntax-highlight</span>のように記述された箇所に、
// シンタックスハイライトを<span>実装</span>する</p>
});
配列に対する実装とほとんど同じように書くことが出来ました。
また、各々のObservable Stream
の責任もコードを一読すれば、ある程度把握できるようになっているのではないでしょうか。
まとめ
という訳で、私がRx
入門前に意識しておくと理解がスムーズになりそうだと思っている事は
- イテレータパターンに対するなんとなくの理解
- オブザーバパターンに対するなんとなくの理解
- 上2つの組み合わせとしてのストリームに対するなんとなくの理解
- ストリームを便利に操るために数多くのAPIが提供されているのが
Rx
の4つです。
Rx
はよくAPI
のあまりの多さ、学習コストの高さでネガティブな紹介のされ方をする場面が多いです。
確かにそういった面は否定しようのない事実なのですが(実際、この記事の実装例くらいのものでも結構手こずりました)、
まずは大量のAPI
とは距離を置いてRx
が提供する価値を享受してみるために、事前に抑えておきたい要素を挙げたつもりです。
更にもう一つ付け加えるならCycle.js / RxJS 入門してサンプラー作ってみたにあるように、オペレータを使いこなそうとしない
というのも挙げられるかも知れません。
とにかく、まずStream
とは何かについて自分なりに腹落ちすることで、入門への心理的な壁を低く出来るんじゃないかと思っています。
良かったドキュメント
最後に私がRx
に入門するにあたって参考にしたドキュメントやサイトを色々載せておきます。
もちろん、この記事を書くにあたっても大いに参考にさせて頂きました。
- The introduction to Reactive Programming you've been missing
- 【翻訳】あなたが求めていたリアクティブプログラミング入門(上の記事の邦訳)
- RxJSで始めるRx(Reactive Extensions)入門1
- RxJS Advent Calendar 2015
- ReactiveX公式サイト
- 各オペレータのドキュメント一覧
- Rx marbles Streamを可視化するとイメージしやすい
- Rx marblesのAndroidアプリ
最近出たRxJS
の本もとてもわかりやすいと思います。文量も多くないし。
この記事に書いた内容も、元々はこの本の1~2章あたりの内容に対する自分の理解をまとめたようなものです。
Reactive Programming with RxJS
正直なところ、Rx
を全面的に採用したアプリケーション構築が主流になる事は考えづらいと思っていますが、Observable Stream
の考え方がシンプルに解決してくれる課題というのも少なくないと思っています。
この記事が、そのような場面に居合わせた時の心構えの一つになれればうれしいです。