Cycle.js / RxJS 入門してサンプラー作ってみた

youtu.be

http://gmork.in/cyro/

サンプラー。 キーボードを押したら音が増える。

  • [A-Z] : 音を入力
  • Shift + [A-Z] : そのキーの音をすべて削除
  • / : 設定パネルの表示 / 非表示

一応ソース fand/cyro · GitHub

Cycle.js / RxJS の感想

Cycle.jsとは

cycle.js.org

Cycle.js は Unidirectional dataflow / Model-View-Intent を実現するWebフレームワーク。 RxJS 及び virtual-dom を使って書かれており、開発者もこれらを利用することになる。 作者の André Staltz 氏は Rx オジサンで、Rx に関するサイトを幾つか作ってる。 Flux や Elm, Famous のアーキテクチャを解説した記事は少しバズったので、見たことある人もいると思う。

staltz.com

Cycle.js について、あとは↓の記事を読んでください。

r7kamura.hatenablog.com

ちなみに、コア部分とDOM部分が別パッケージになってたり、scoped package で配布されててかっこいい。

ReduxとRxJSは対照的なアプローチ(主観)

Redux と Cycle.js / RxJS は対象的なアプローチと感じた。 Redux では state という世界のスナップショットを積み上げて状態遷移を実現する。 対して Cycle.js は、Observable という因果関係を撚り合わせて、state を生み出す Observable を作り上げる。 state が横糸なら Observable は縦糸だ。

Redux では、世界の真実がすべて1枚のJSONで表現される 。 開発者は各自ピュアな関数を書いて、真心込めて state を作っていく。 僕は最近 オレオレ Flux 実装か Redux しか使ってないので、「stateを作る関数を作る」という考え方に慣れきってしまっている。

それに対し、Cycle.js / RxJS では Observable というピュアっぽいなにかを繋ぎあわせると自動的に state が出来上がる。 Cycle.js でも view に渡す寸前に Rx.Observable.combineLatest() で静的な state を生成したりするんだけど、Flux とは要求される考え方が大きく異なる。

僕がほぼ何もしなくても、state はめまぐるしくリアクティブに変わっていく……

Flux……なのか?

Cycle.js の作者が flux-challenge というリポジトリ作って、「僕の考えた最強に美しい Flux 設計」大会をやってる。

github.com

Cycle.js は Flux の一種って事になってるのか? Redux も Flux と違うけど、Cycle.js はそれ以上に Flux から遠ざかってると思うんだよな……。

↓のツイートは、 Dan Abramov が Redux を作る前、Flux で良い設計を実現するための考え方を説明したもの。 André Staltz, Dan Abramov, Michael Jackson らを巻き込んで、 Flux の問題点について会話している。 André は Flux を強く批判していて、それに対するアプローチの一つとして Cycle.js に触れている。

driver の考え方は Flux にも応用できるのでは

Cycle.js の根幹をなすのは、その名の由来にもなっている「Human-Computer」の循環的なデータの流れだ。 これは単に Unidirectional というだけでなく、Model の外側の知識は常に driver を通じて Model にもたらされる、という特徴を持つ。

f:id:amagitakayosi:20151012022047p:plain 引用元 : Cycle.js

Cycle.js では View の描画に virtual-dom を利用し、イベントの監視は別に DOM driver を用いる。 DOM driver はDOMイベントから Observable を生成する。 これらの Observable はモデル的に意味のある単位へまとめられ、Intent に変換される。

これだけなら Flux で Action を生成するのとそう変わらないが、問題は非同期 Action に相当する部分だ。 Flux では http通信などの非同期的な処理は「非同期Action」で行うのが良いとされている。 通信開始、終了、エラーの各時点で Action を発行するという方式だ。 このとき、通信のタイミング制御など複雑な知識が必要になるとき、モデルの知識が ActionCreator に過剰に漏れてしまう事がある。 ActionCreator は元々ドメイン知識を持っている、とはいえ、できるだけシンプルなイベント発行屋さんに保ちたい。

Cycle.js では http通信に HTTP driver を用いる。 Computer から HTTP driver に通信を依頼し、通信の結果が Human からのレスポンスとして再度 Computer に渡される。 (このとき「Computer」と「Human」は明らかに名前として適切ではない。「モデル」と「現実世界」とでも呼んだほうがいいだろう)

結果、処理の流れは↓図のようになる。

引用元 : r7kamura/cycle-fetcher-driver · GitHub

データはモデル-現実世界の間を2回転し、HTTPリクエストに関する知識をモデルから追い出せる。

この考え方を応用すると、Flux でも2回転式の非同期Actionが行える。 ↓図で Component と呼んでいる部分に、driver に相当するロジックを実装すれば良い。

ただ問題は、Flux では Component に渡されるのは state であって、イベントではないということだ。 Reactコンポーネントの場合、state に変更があったときは自動で差分を計算して必要な処理をしてくれる。 自作のクラスを state の変更に反応させるには、↓のコードのように state に適当なプロパティを持たせればいいのかもしれないが、 if (state.xxx) とかして Store 側で丁寧に xxx を管理する必要があって難しい気がする。 Reactコンポーネントではない物を state の変更に反応させる shouldUpdate みたいな仕組みがあれば良いのかもしれない。

import fooAction from '../actions/fooAction';
import FooStore  from  '../stores/FooStore';

class HttpDriver {

  constructor () {
    FooStore.on('change', (state) => {
      if (state.http.fetchUrl) {
        this.fetch(state.http.fetchUrl);
      }
    })
  }

  /**
   * @param {string}  url
   * @return {Promise}
   */
  fetch (url) { 
    fetch()
      .then(response => response.json())
      .then(json => fooAction.receiveHttp(json));
  }

}

export default HttpDriver;

RxJS むずい

僕は以前に2回くらい RxJS に入門しようとして挫折してる……。 その時は「なんだこのjQueryオバケ!!」って感じでキーボード殴ってた。

今回、改めて苦労しつつ使ってるうちに、少しは良さがわかってきたような気がする。 RxJS 入門するときは、以下の点に気をつけると良いと思う。

オペレータを使いこなそうとしない

RxJSでは、オペレータによってObservableを繋ぎあわせて目的を達成するんだけど、このオペレータがやたら沢山ある。 容量削減版の rx.lite.js の方だけでも93種類あった。

これらは習熟していくと便利に使えるのかもしれないけど、慣れないうちは混乱の原因になる。 直面している問題を解決するオペレータを探すのに時間を取られてしまい、一つオペレータを書くだけでヘトヘトになってしまう。 最初は↓で述べられているような基本的なオペレータだけ使うように意識すると良いだろう。

RxのAPIの多さは弁護しようがないんだが、あれはLINQ to Eventsなので、非同期なイベントの集合に対してだいたいArrayに似たcollection操作(というかクエリというか)をするのがコンセプト。なのでmap/filter/flatMapあたりを覚えればだいたいなんとかなる。これに+して非同期操作なので、待ち合わせ系のメソッドを2~3覚えれば良いだろう。Promiseの連続版であるので、これも all/some/raceに相当するメソッドを覚えれば良い。

Re: Rx.js, Immutable.js について - snyk_s log

最初は行儀の悪いコードで書く

FRPな考え方に慣れなきゃ!」って無理に頭ひねって書こうとすると永遠に完成しない。 入門したてで Rx 流のコード書けるわけないので、気楽に汚いコード書くと良さそう。

今回僕が書いたコードは、見れば分かるとおり本当に最悪な汚いコードなんだけど、これでもめっちゃ時間かかってしまった。 新しい事やる度に30分くらい頭ひねって、良いアイデアが思いつかなければ最悪なコードで書いた結果、Model 部分は状態変数だらけになった。 複雑なイベントは Rx.Observable.fromEvent(eventEmitter) を使って書くと割と素直に書ける。 Intent 作成するところは RxJS こねこねして、Model は無理しないようにすると良い。

関係ないけど

と、新しいライブラリ触る度に音の出るアプリとか画面光るアプリ作ってるんだけど、あんまり筋良くない気がする。 サーバとの通信とかしないので実際のWeb開発で活きる知識にもならないし、React系のライブラリでCSSアニメーション以上のアニメーションしようとするとライブラリが面倒見てくれないので「俺は何をやっているんだ……??」となる。

(◞‸◟)