Rx(JS)に入門する前に知っておきたいN個のこと

MMM Corporation
mmmuser

アドベントカレンダードリブンダイエットに失敗中の小飼です。
走っているのに中々痩せません。どうなってるんだ。。。

※この記事はRxJS Advent Calendar 2015の第二十日目の記事です。

概要

最近チラホラRxJSについての記事を見るようになってきました。
非同期処理を簡潔に書けることについてのポジティブなポストを見かける一方で、主に学習コストの面でネガティブな意見が目立っていることも否定できません。

私は業務でバリバリRxJSを使いこなしている!というようなレベルでは到底ありませんが、入門ブログ・書籍などを読んだり簡単なコードを書いてみて、事前に〇〇について知っていると理解が早そうと思った点がいくつかありました。

そこで本稿ではRx(JS)に入門する前に知っておきたいN個のことと題して、事前に知っておくとスムーズにRxJSに入門出来るんじゃないかなと思うことを書いていきます。

と言ってもご紹介したいのは基本的なデザインパターンについての事なので、既にご存知の方も多いと思います。
ただ、そういった方でも事前にこのデザインパターンに強く関連があるのだと前もって心積もりをしておくことで、スムーズな理解が得られるかも知れません。

また、説明を簡単にするために、諸々を単純化し過ぎている箇所があるかも知れません。
そこはひとえに私の能力不足によるところですので、ガチな方々にはそこら辺をご容赦お願いします :bow:

前提: 非同期処理は難しい

JavaScriptによるアプリケーションを作成するにあたって、非同期処理というのは避けて通れません。
これは、いつ発生するかわからないユーザーからの入力をevent loopで捌いていくJavaScriptの言語設計の宿命と言えます。

この非同期処理の操作というのは一般にわかりづらいものになりがちです。
いつ完了するのかが予め分かりませんし、複数の非同期処理がある場合にも完了順序は保証されません。
また、実アプリケーション上ではしばしば完了しない可能性もあります。

非同期処理ってどういうこと?JavaScriptで一から学ぶ

そこでJavaScript界隈では、この非同期処理を分かりやすく記述するための様々な技法が取り入れられてきました。
例えばPromiseGeneratorを使った同期処理風な記述方法などです。

ただ、実アプリケーションではしばしば複雑な非同期処理の組み合わせを実装する必要に迫られます。

例えばユーザーのキーボード入力に特定の文字列を検知したタイミングでサーバーにリクエストを発行したかったり(rx, cycleなどのnpmライブラリに存在する単語が入力されたら、該当しそうなライブラリの情報をサーバーから取ってくる、みたいな)、サーバーへのリクエストを複数回行ってみた内の特定のレスポンスをを使用したい(ジョークAPIに?random=trueのようなクエリ付きでリクエストを投げた時に、Chuck Norrisという文字列を含まないレスポンスだけを採用したい)というようなケースです。

もちろんPromiseGeneratorのような慣れ親しんだ概念でも充分に実装可能ですが、往々にして不安定で複雑な実装になってしまいがちです。
そんな非同期処理の組み合わせを、もっとシンプルで安定した実装にするためのアイディアが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 StreamNode.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;

イベントリスナーによる実装

これを従来のオブザーバパターンによるイベントリスナーで単純に実装してみるとこんな感じになりました。
(例えばFluxActionCreators層に実装したと読み代えて頂くこともできると思います)

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

resultStrappearedCountなど、イベントリスナーの外部スコープに存在する変数を使った実装になっており、ちょっと不安な気持ちです。
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に入門するにあたって参考にしたドキュメントやサイトを色々載せておきます。
もちろん、この記事を書くにあたっても大いに参考にさせて頂きました。

最近出たRxJSの本もとてもわかりやすいと思います。文量も多くないし。
この記事に書いた内容も、元々はこの本の1~2章あたりの内容に対する自分の理解をまとめたようなものです。
Reactive Programming with RxJS

正直なところ、Rxを全面的に採用したアプリケーション構築が主流になる事は考えづらいと思っていますが、Observable Streamの考え方がシンプルに解決してくれる課題というのも少なくないと思っています。

この記事が、そのような場面に居合わせた時の心構えの一つになれればうれしいです。

AUTHOR
デロイト トーマツ ウェブサービス株式会社(DWS)
デロイト トーマツ ウェブサービス株式会社(DWS)
デロイト トーマツ ウェブサービス株式会社はアマゾン ウェブ サービス(AWS)に 専門性や実績を認定された公式パートナーです。
記事URLをコピーしました