ReactiveX: コレクションに内包された複数のObservableの変更を受け取りたい場合にどうするか

まあサンプルコードはRxJSベースで書いてるわけですが.

Reactのようなone-way data flowなテンプレートエンジンとObservableを用いて, フォーム形式のアプリケーションを作っている場合, Observableの入れ子で表したいデータ構造をつくりたいことがある. たとえば, テーブルの各セルをそれぞれ別に編集できるような, 擬似的なエクセルもどきのフォーム. Observableを使って表そうとすると, 以下のような定義をすることになるだろう.

// テーブルの各行に相当. 中身が変更されうるのでObservableで表したい.
class Row implements Observable;

// テーブルを表すRowの集合. Array<T>やSet<T>でも良いが, どれが編集されたかを伝える都合上, 
// 各行のIDとなるキー_K_で逆引きできるMapとする
type Table = Map<K, Row>;

// テーブルそのものを示すObservable.
// これをsubscribeしてReactに渡してやれば, テーブル形式のViewが出来上がる…ということにしておこう
type ViewModel = Observable<Table>; // Observable<Map<K, Observable>>

このようにObservableが入れ子になるデータ構造において, Map(Collection)の中に包まれたObservableの変更を, 最上位のObservableにどうやって伝えるか?ということについて書いてみる. 実用するには必ず困る事案なのに, この手の内容について, あまり先行する情報がない気がする. Proof of Conceptレベルなので最適化が済んでない場合はご容赦を. そもそものアプローチとして, データ構造などを変更して, もう少しうまくやる方法もあるかもしれない点には注意.

まあ結論を先に述べると:

  • 基本的な指針は, 内包されている側の変更を内包している側に伝える事
  • Observableの数が静的に決まる場合はそんなに困らない
  • 困ったら「Observableをつないでいくスタイル」にこだわらずに, 古典的なObserverパターンに則ってみる(なぜならRxはObserverパターンを下敷きにおいているから)

に要約されると思う.

collectionの中に内包されるObservableが静的に決定される(要素数が固定)場合

これは全く難しくない. 内包される変更をがあったら, collection全体を流しなおすようにするだけだ. だいたいこんな感じ.

import {Observable} from 'rx';

class FixedLengthTableRow {
    a: Observable<A>;
    b: Observable<B>;

    constructor() {
        this.a = ...;
        this.b = ...;
    }

    asObservable: Observable<void> {
        return this.a.merge(this.b);
    }
}

class FixedLengthTable {
    private _collection: Observable<Map<K, FixedLengthTableRow>;

    constructor() {
        let updater = Observable.never();
        const collection = new Map();
        for (let i = 0; i < 10; ++i) {
            const r = new FixedLengthTableRow();
            collection.set(i, r);
            // コレクションが内包する全ての要素の変更を受け取るチャンネルを開通させ, 巻き込んでいく
            updater = updater.merge(r.asObservable());
        }
        
        // 内包するいずれかの要素の変更があったら, 状態が変わったものとしてコレクション自体を後続に流しなおす.
        this._collection = updater.map(() => collection);
    }

    asObservable(): Observable<Map<K, FixedLengthTableRow> {
        return this._collection;
    }
}

const table = new FixedLengthTable();
table.asObservable().subscribe((collection) => {
    // collectionの中身が更新されると勝手に流れてくる
});

collectionの中に内包されるObservableが動的に決定される(要素数が可変)場合

これはちょっと面倒臭いように思える. 一度構築したObservableのチェーンは動的に繋ぐものを増やしたり減らしたりできないためだ. Rx入門の多くで語られる「FRP way」に思考を奪われると本当に難しく思えてしまう.

が, これはSubjectを用いて, 古典的なobserverパターン的に解いてやれば良い. こんな感じ.

class DynamicLengthTableRow {
    a: Observable<A>;
    b: Observable<B>;

    constructor(notifier: Subject<void>) {
        this.a = ...;
        this.b = ...;

        const updating = this.a.merge(this.b);
        updating.subscribe(() => {
            // 状態変更が起きたら変更を通知する.
            notifier.onNext();
        });
    }
}

class DynamicLengthTable {
    private _receiver: Subject<void>;
    private _collection: Observable<Map<K, DynamicLengthTableRow>>;

    constructor() {
        // コレクションが内包する全ての要素の変更を受け取るチャンネルを用意する
        this._receiver = new Subject();

        const collection = new Map();
        for (let i = 0; i < 10; ++i) {
            const r = new DynamicLengthTableRow(this._receiver);
            collection.set(i, r);
        }
        // 内包するいずれかの要素の変更があったら, 状態が変わったものとしてコレクション自体を後続に流しなおす.
        this._collection = this._receiver.map(() => collection);
    }

    receiver(): Subject<void> {
        return this._receiver;
    }

    asObservable(): Observable<Map<K, DynamicLengthTableRow>> {
        return this._collection;
    }

    add(row: DynamicLengthTableRow): void {
        this.add(newKey, row);
    }
}

const table = new DynamicLengthTable();
table.asObservable().subscribe((collection) => {
    // collectionの中身が更新されると勝手に流れてくる
});

// こんな風に増減できる
const row1 = new DynamicLengthTableRow(table.receiver());
table.add(row1);

リアクティブとかファンクショナルとか言わないReactiveX入門

しばし見かけるReactive Extensions(ReactiveX, Rx)に関する説明の多くはファンクショナルだのリアクティブだのモナドといったキャッチーなフレーズを使っている。けれども、そういう方面に馴染みのない人を相手にして、そもそもの概念的に何を解決したかったものなのかという説明があんまりない気がした。ユースケースを伴った説明も局所解すぎて現実における使い道がわかりにくい、というか誰も彼もデータバインディングしか例に出さないのはどうなんだ。これはストリームの川?それは表現形態であって実態を表しているとは言い難いだろう。

放置していても誰も書かない気がするし、神秘的な霊験と共に語られても全く役に立たないし、自分の思考の整理と(幾らかは同僚への説明も兼ねて)書いてみることにする。

もしかすると.NET界隈あたりでは過去にやりつくしたネタの再生産かもしれないし、ラジオなどの非文章媒体で既にあるのかもしれないけれど、見つからなかったので、まあいいや。


プログラミングの手法としてイベント駆動でコールバックを呼び出すパターンが存在する、というのは周知の事実。これは「何かが変化したら、それが変化したことを任意の対象に対して通知する」のに役立つ。典型的なのはGUIプログラミング。ユーザーがクリックしたことがイベントとして通知され、それを受けて処理を行う。ネットワークリクエストの結果を受け取るコールバックも、レスポンスが帰ってきたという局所的なイベントを受けて動作する点で変わりはない。

プログラミング的にもイベントが飛んでくるという抽象化がなされている方がやりやすいことは、それなりにある。もしイベントがないと延々とポーリングをして状態の変更を確認し続けなければならない。面倒臭い。

ところが、この古典的なイベント処理には問題がある。イベントというのは本質的に非同期に突如としてやってくるものであり、それぞれがバラバラに飛んでくる(離散的に、と言い換えても良いかもしれない)ものであるのが殆どだからだ。ネットワークレスポンスの終端イベントは開始イベントの後にやってくる、という程度の順序はあるけれど、個々のイベント自体は、自信がどういう順序関係で発行されたのかを情報としては持ち得ていない。

イベントが全部バラバラに飛んでくる以上、「画面がリサイズされた後にクリックが起こった場合」というような状況を適切に処理したい場合、各イベントのユーザーであるプログラマは、自分でフラグを立てるなどをしてイベントの順序を管理するしかない。イベントAに対応するフラグA’を立てて、その状態でイベントBが来たらB’を立てて、A’とB’の両方がtrueであれば後続に処理をつないでいきetc。フラグが増え続け、破綻する日は近い。

イベントAとBが両方とも来たことを示すCというイベントを発行する、という手法もあるが、これはイベント粒度を細くすればするほどやることが増えていくし、各イベントが取り扱う変化の範囲で悩み始める。

このイベントが多すぎる・関係性を持ち始める問題は、わかりやすく例えれば、GUIがリッチになるにつれ問題となっていく。GUIの変更・ユーザーの操作それぞれが条件となり、次に起こすべき変更もさらに条件となっていく。しかも、これがモバイル端末の上ともなると、ネットワークの状態、バッテリーの具合、環境光の変化などが出てくる。クライアントではなくサーバー側であれば、クラスタ内のノードの死活状態やログの送受、広くはクライアントからのリクエストもイベントとみなすことができるだろう。混沌としてくる。

こうした状況に対してLINQ to Eventsとも呼称されるReactiveXが提示した解決策は、イベントは離散的なデータの集合であり、それらから任意のイベントの関係性を取り出す方法があれば良いということ。度々例示してきたように、一見バラバラで独立している個々のイベントにも、実際に利用する際には関係性が存在している。その関係性をコードとして明示しクエリとして処理することで、コードを通して人間にも読める形式で、機械的に結果を取り出すことを可能にした。この結果、複数のイベントの関係性から別のイベントを生成することが容易になり、イベントに紐づく変化の範囲についてコードを書く側が悩む必要もなくなった(必要に応じてイベントを細かく割りやすくなった)。

これと同時にスケジューラという概念を導入することで、取り出したイベントを後続に伝えるタイミングや、イベントを発行する対象のスレッドなども一連の流れの中で制御できるようにした。

それと、イベントを文字列やsymbolのような固定値ではなくオブジェクトとし、構文ないしメソッドの形式でイベントの関係性を表現したことで、コード解析技術を通してイベントの関係性の解析・図示や入力補完といったことが可能になった。これにより、アプリケーション規模が巨大になっていってもIDEなどを通してコードを掌握しきることができるので、ある種のgoto jumpであったイベントハンドリングがコード上に静的な形式で落とし込まれることになった。

私のブログを読んでいる人はJavaScriptの読み書きをする人が多いと思うので、JavaScriptのPromiseとの関連で説明しておくと、Promiseは原則として1回しか自身の状態を変更できない = 1回限りのイベントにしか使えない(なので、実質的にはpull型で動き始める)。何回でも発生しうるイベント全般に対する適用ができないから、Promiseだけではイベントの関係性は表現し得ない。

さて、先述のような抽象化を実施するために、Rxの実装の裏側では多くの一時オブジェクトを生成するなどしているため、実行時のコストがかかるように思える。だが、ハードウェアの進歩とコンパイラVMの進化、及び抽象化に伴うプログラミング時の生産性の向上と合わせると、アプリケーションのレイヤーではコストとして無視するという選択が可能になった。コストが無視できない環境では従来通り頑張って書けばいいだけで、事態が悪化したわけではないし、言語そのものを乗り換える(JS -> Java/C# -> Cpp)ことでも案外解決できたりする。

また、イベントを発行することに伴うプログラミング時の負担が減った結果、状態の変更をイベントとして気軽に通知しやすくなった。これにより、ある意味では副作用を起こしやすくなった。副作用による問題の一つは、ある状態に依存している(ある状態であることを仮定している)後続の処理群が、前提条件の変更を適切にハンドリングできない点にある。故に、気軽に状態が変更された事実をイベントとして伝え、再度計算し直すことで、全体としての整合性を保てるようになる(だからと言って、気軽に手当たり次第に副作用を起こしていいかというとそれは別)(再計算によるコストも、前述の通り実行環境の進化で無視できるという前提に立っているので、無視できない変更が起こるケースは再計算せずに済ませるようにした方がいい)。

ReactiveX.ioのイントロに「俺たちはFunctional Reactive Programmingじゃない」って書いてあるのは、各自読んでもらうとして、だいたいこんな感じ?

ソフトウェアの設計に思いを馳せた何か

http://www.slideshare.net/saneyuki/my-thoughy-about-beyond-flux

(主にGUIの)アプリケーション設計的なものをぼんやりと考えていた時に、色々思いついたことを書いた。特にどこかで発表するという予定もないんだけど、作ったからには手元でタンスの肥やしにするのも勿体無いので貼っておく。「まあ、そうだよね」とかそういう感じでしかないし、何か真新しい内容があるわけではない。

結構散漫な話なので上には書かなかったんだけれども、ソフトウェア開発において人はよくベストプラクティスを求める傾向にあるけれども、現実には「悪くはない」程度の解を選んだり、バッドプラクティスやアンチパターンを避ける方が総じて上手くいくように感じている。理想形を探り求め続ける限りに天井は無いが、「良くも無いが、最悪に面倒臭いことは回避している」であれば下限が決まっているので、程々に上手くやれるという認識でいる。

それと、

ソフトウェアを設計していていくつかの設計案のうちどれがいいのか決めかねるようなときは、「とりあえずOSがやっていることをまねる」というのが良いノウハウな気がしている。だいたい我々が直面するような問題というのは、OSがすでに直面していて何らか解決されている。

という話は真であると思う。

最近のJavaScriptはスクリプト言語のジャンルで必要な機能がだいたい揃った気がする(言語的な範囲では)

少し前にTypeScriptと素のECMAScriptとfb::JSXを混ぜてcompile -> linkする記事を勤務先のブログに書いたのだけれども、それ以後に色々思った小話を一席。

先述のエントリで示した構成を趣味プロジェクト含めて2~3度試した結果、現代のJavaScriptは書き捨て的なスクリプトから、(漸進的な)静的型付けまで、言語機能的なところではソフトウェアの規模に応じて、自由にスケールアップできるという実感を得るに至った、という確信を得た。インクリメンタルに移行できるので新旧コードの混在もできるし、素のJSじゃないとできないものは、それにやらせればいい。これはTypeScriptなりbabelといったcompilerやAST操作ツール群の成熟と、npmエコシステム、ES6というベースラインの更新が絡み合っている。

こうしてコードベースの規模に応じて自由にあれこれできるのは非常に楽だなと思う所存。それに今のJavaScript処理系(Chakra/SpiderMonkey/V8/JavaScriptCore)はどれも相当に速いので、速度が直接的にボトルネックになるよりも、コードや設計のマネージの方が主題になることの方が多いと思う(これは計算量がワーストケースに落ちない戦略・設計的なマクロなアプローチを含む)。micro optimizationが必要なケースもあるだろうけど、だいたいは

  1. ボトルネックを計測
  2. 頑張る
  3. 頑張って無理なら諦めてもっと速い言語を使う

な感じになるだろうし。

TypeScriptの提供する以上の言語機能が欲しい場合はどうするか?という問題は、素直に言語を乗り換えるのが良いのでしょう。あれより高水準な表現力とか静的型付けを持つ言語はだいたいJVMで動く言語かネイティブコードを吐いて動かす言語になるし、それが必要になる場合は速度面でもJavaScriptの適応可能範囲を超え始めるだろうから、乗り換えの時期なのだろう。

例外として、TypeScript以上の型表現を持ったAltJSというとScala.jsとかがあるけれども、各処理系のfirst tierがECMA262である以上、なるべくそれに近い方が色々と予測性が高いのとTypeScriptも1.6な今ならあれで多くのケースは解決すると思うので、私はあまり興味がない。大抵のケースは、Scalaが欲しいなら素直にon JVMな実装使った方がいいと思う。JSVMの上で語るにしても、WebAssmebly(のGC intergration)が来てからじゃね?

話が逸れたけど、Closure Compilerとかに頼らずとも、大きなソフトウェアを作るまでの言語機能のスケールができるようになったのは、いい時代になりましたね、という趣。

RxJSとRxJavaとでは旨味のコクが違うように思う

Rx(もしくはReactive ExtensionsないしReactiveX)には

  • Observable
  • Operators
  • Subject
  • Scheduler

と云った概念がある(分類はReactiveX.io調べ)。

で、Rxを用いた非同期処理を実際に行うにあたっての実行計画を制御するための要素が Scheduler に当たり、実行タイミングや実行スレッドの制御をこれで行うことでRxはLINQ to Events足り得ており、時間軸に関係するイベントもテスタブルにしている……みたいな話はRxをかじった人なら知ってると思います。かじってない人はそんなもんだと流してください。

で、ここしばらくRxJSでいろいろやっていたのだが、このSchedulerの重みが、JavaC#とは全く違うのであろうという仮説を抱くに至った。

メモリ空間共有型のスレッドモデルを持つJavaC#に於いては、非同期処理・並行処理を行うにあたってはスレッドを適切に掌握する必要が有る。で、RxはSchedulerがあるおかげで、この面倒臭いスレッドの概念もそれなりに統合して取り扱えるようになっている、というのが売り文句の一つ。

一方のJavaScriptにはメモリ空間を共有するようなスレッド機能はなく、あるのはshared nothingなWorkerであり、基本はシングルイベントループをぶん回し続けるモデルになっている。そして、多くの処理は(特にNode.js方面のAPIは)非同期を前提としたAPIモデルとなっている。

故に、RxJSが提供するSchedulerは、同一スレッド上での実行タイミングの操作とテストダブルであり、スレッドをまたぐ処理への抽象化を加えるようなインパクトは無い。

なので、JavaScriptにはPromiseもあるしSchedulerのインパクトも無いし、JavaC#の場合に比して「冒険してまで導入したい」成分は薄れているように思った。人間、華々しさや煌びやかな様子に負けて手を出すのは常なので、インパクトが無いと起爆剤が欠けるよなあ。

breaking changeの整理

生きているソフトウェアに付き物の破壊的変更(breaking change)であるが、一口に破壊的変更と言っても、実際は複数の意味を包含している。 なお、「生きているソフトウェア」では主語が大きすぎるので、ライブラリであったりランタイムについての話とする。

一般的に破壊的変更という場合、以下のどちらかとなるだろう。

  1. セマンティクス・挙動の変更を伴う変更
  2. セマンティクスの変更を伴わないが破壊的とみなされる変更
    1. APIの名前が単純置換された

1は明らかに破壊的である。ソフトウェアに対して少なからぬ書き直しが発生するし、単純置換では済まないケースの方が多い。 そもそもの設計思想やアプローチに修正が加えられたので、前と後とでは違うものと言っても過言ではなく、十分に破壊的と呼ぶに足りえる。

しかし、2のケースは一概に破壊的と言えるかは怪しい。辞書的には破壊的だが、我々の思い浮かべる大規模な書き直しを強いられるような破壊性が あるわけではないからだ。

2を破壊的と呼ぶか否かは、対象となる環境の置かれたバックボーンに依存することとなる。例えばRustのような強い静的型付の言語では、 コンパイル時に静的解析が発生するため、APIが消えただの名前が変わっただのは只のコンパイルエラーとして処理される。 標準ライブラリに絡む変更である場合はコンパイラが警告を細かく出してくれることもあるだろう。この場合、破壊的なリスクは早期に発見できるので、 相対的に破壊的変更は軽度の問題と化す。

逆に、実行するまでわからないような言語環境(典型的にはJavaScriptが該当する)では、このリスクは格段に跳ね上がる。 動かさないとわからない上に、実際にそのパスに入って実行されないわからない変更である場合、「やってみるまではわからない」ブラックボックスと化す。 エラーで落ちれば御の字だが、実は上方のtry-catchがエラーを握りつぶしてしまっており、前提条件が崩れたままに後続の処理に入り……などいう袋小路に入ると本当にどうしようもない。 この場合の破壊的変更のリスクはセマンティクスの変更と差はない。なので、我々はこれを早期に発見できるように頑張る必要がある。 semantic versioningは絶対遵守されるべきである。道徳律を超えた規範にして、これに逆らうライブラリ作者は異端審問で火刑に処されても構わない…というのは、やりすぎだが、 こうした自身のソフトウェアの破壊的な挙動につながる事態は想定されるべきとなるし、それを避けるためにテストコードの充実やカバレッジといった指標の整備、 CHANGELOG.mdを記述する道徳律の作成コミュニティの規範と正義を整えて共同体秩序を守る必要がある。かなり面倒くさい。できればあんまりやりたくない。

ここらへんの文化圏の交差とか色々残りはあるんだけど、書いてる最中にやる気がなくなったので寝ることにする。おやすみなさい。