読者です 読者をやめる 読者になる 読者になる

uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかFregeとかJavaとか -

Reactはリアクティブプログラミングなのか?

React JS JavaScript SPA FP FRP

Reactとは

Reactは、Facebookが開発した、JSのUIフレームワークもしくはライブラリです。Reactが提供する中核機能は以下です。

  • イミュータブルなUIビルダー
    • Virtual DOMによる効率的更新
  • 上記に付随するイベントハンドラ群を定義編成していくための方法論
    • React単体ではコールバックの組合せで、Fluxの一部として使用するとオブザーバーパターンで実現

効用は、再利用性と保守性・可読性向上です。特に、Reactで作成した画面部品のコンポーザビリティが高く、細粒度のUI部品再利用の発展充実が期待されます。作りは通常のJSクラスライブラリであり、覚えたりすることは多くありません。

設計をとりもどす

Reactが解決しようとする問題は、大規模化するSPAにおいて、大域状態をDOM内の値として管理し、無数のイベントハンドラがただあるだけ、といった「設計不在」への対処です。Reactは「UIはこう設計しようぜ」と導きであり、設計を大事だと思う一部の人には大受けします。やんややんや。

Reactでやってみる

さて、以前、以下の記事では、関数型リアクティブプログラミング(FRP)を実現するAlt JSであるElm言語を用いて、マウスストーカーというマウスを追い掛ける★を表示するプログラムを実装しました。

uehaj.hatenablog.com

上記の元になった記事は、Beacon.jsで実装した記事なのですが、ElmもBeaconJSもリアクティブプログラミングを実現する技術です。ElmのHtmlライブラリ「Elm-html」はReactと同様にVirtual DOMをレンダリング効率化技術として採用しているので、Reactだとどうなるかなと思ってやってみました。

以下がReactでのマウスストーカーの実装です。Resultのところを押すと実行できます。

画面キャプチャは以下です。

gyazo.com

コードは以下になります。

'use strict';
var React = require('react');
var ReactDOM = require('react-dom');

var Star = React.createClass({
    getInitialState: function() {
        return {x:30, y:30}
    },
    setPosition: function(x, y, refs) {
        this.setState({x:x, y:y})
        if (this.props.followedBy != null) {
            setTimeout(()=>{
                refs[this.props.followedBy].setPosition(x, y, refs)
            }, 100);
        }
    },
    render: function() {
        return (
            <div style={{position:'absolute', left:this.state.x, top:this.state.y, color:'orange'}}>
                ★
            </div>
      );
  }
});

var Screen = React.createClass({
    onMouseMove: function(ev) {
        this.refs._1.setPosition(ev.clientX, ev.clientY, this.refs)
    },
    componentDidMount: function() {
        document.addEventListener('mousemove', this.onMouseMove);
    },
    componentWillUnmount: function() {
        document.removeEventListener('mousemove', this.onMouseMove)
    },
    render: function() {
        return <div>
            <Star ref="_1" followedBy="_2"></Star>
            <Star ref="_2" followedBy="_3"></Star>
            <Star ref="_3" followedBy="_4"></Star>
            <Star ref="_4" followedBy="_5"></Star>
            <Star ref="_5" followedBy="_6"></Star>
            <Star ref="_6" followedBy="_7"></Star>
            <Star ref="_7" followedBy="_8"></Star>
            <Star ref="_8"></Star>
        </div>
    }
});

ReactDOM.render(<Screen />, document.getElementById('container'));

Elm-HtmlとReactの対応

Elm版では、DOMベースではなくてElmのCanvasベースのAPIを呼んでいたので、本当の比較は実はできません。なので「Elm-Htmlで作っていたら」という想定をした上での比較になりますが、以下のとおり。

Elm-Html React
Signalに包まれた値 Reactコンポーネントに包まれたstate
SIgnal Htmlを返す関数 stateを持ったReactコンポーネントのrender()
Signalには包まれないHtml値を返す関数 stateを持たないReactコンポーネントのrender()。実際、stateを持たないreact componentは、React 0.14ではクラスではなく関数として書ける。
純粋関数でのビュー構築 コンポーネントAPIのrenderメソッド
mainでの、純粋関数をSignal.map*してシグナル値を当ててSignal Htmlを得る操作 ReactDOM.renderComponent()
on*で設定したイベントハンドラからport経由でSignalをキック、そしてそのSignalに結びついたfoldpが起動されるまで コンポーネントでの各種イベントハンドラを実行してthis.setState()
Elm Archtecture ルートコンポーネントのみにstateを置くという方針
Signalの操作関数群(Time.delay, Signal.sampleOn..) なし
- flux

このように対応付けることができます。一見すると、両者はとても似ているとも思えます。しかし、その類似性の多くは、両者ともVirtual DOMを採用し、さらにやっている処理が同じであることに由来しており、必然です。最終的にはJS上で、同じようにDOMイベントを処理して、同じDOMを描画する以上、対応付かないはずがないのです。

なので差異の方が自分としては興味深いところです。たとえば、Elmでは純粋関数しかないので「直接Signalを発火できない(setStateできない)」という制限が大きく、Reactではハンドラ内のsetState()で済むところをport経由でTaskを起動したりしないとなりません(MessageやAddressを経由した暗黙にせよ)。また、ElmではTaskの定義場所と、そのタスクをキックするハンドラなどがポートを介してばらばらになりますが*1、両者を一つのクラスとして書いて対応付けるReactの方が私にはわかりやすく感じました。

ReactはFRPか?

まず、FRP(関数型リアクティブプログラミング)の定義ですが、こちらを参考にすると、「動的であり変化する値(すなわち、“時間とともに変化する”値)をファーストクラスの値として、それらを定義し、組み合わせ、そして関数の入力・出力に渡すことができる」という感じでしょうか。

とすると、ReactはFRPと言えません。ぜなら時間に伴なって変更される値(Time Varrying Value, Elmで言うSignal)に対する抽象操作ライブラリが整備されていないし、一次イベントを組み合わせて、新たなSignal(二次イベント)を構築する方法も提供されておらず、イディオムとしても確立されていないからです。

たとえば前述のマウスストーカーで、Elm版では★が前の★の位置においつこうとする動作を、Time.delayを用いて以下のように記述できました。

          trace = Time.delay 100 -- 100ms遅延を与えたSignalを生成
          p1 = Signal.sampleOn AnimationFrame.frame Mouse.position -- 最初はマウス座標を追う
          p2 = trace p1 -- 以降、一個前の座標を追うようにする

上ではtraceという一時関数を定義していますが、それを展開すると

          p1 = Signal.sampleOn AnimationFrame.frame Mouse.position -- 最初はマウス座標を追う
          p2 = Time.delay 100 p1 -- 以降、一個前の座標を100ms遅延を与えて追う

になります。Signal(ElmにおけるファーストクラスのTime Varrying Value)であるマウス位置(Mouse.position)と、同様にSIgnalであるAnimation Frameのサンプリング(AnimationFrame.frame)を、合成操作「Signal.sampleOn」で組合せ、さらに「引数のSignalを500ms遅延して発火する操作Time.delay」で、ファーストクラス値として操作・合成しています。

かたや、Reactで同じことをするのは、

    setPosition: function(x, y, refs) {
        this.setState({x:x, y:y})
        if (this.props.followedBy != null) {
            setTimeout(()=>{
                refs[this.props.followedBy].setPosition(x, y, refs)
            }, 100);
        }

の部分で、要は、位置の変更時(setState())に、setTimeoutで自分を追跡する★の位置を指定時間遅延させて発火させています。

最終的にはFRPでも実行されるのはこれだけです(実際、ElmはJSにコンパイルされる)。FRPに魔法は別にありません。イベントハンドラとコールバックの組合せで実現しなければならなかったことを、抽象操作として利用できるので便利! 偉い! 見易い! わかりやすい!ということです。逆に言えばFRPはそれだけのことなので、その抽象層がなければFRPではない、と言えると思います。

もっとも、Reactを含むフレームワーク(もしくはアーキテクチャ)であるFluxは、Time Varring Valueの合成操作や二次イベントをイディオム的にうまく実装できるのかもしれません。たとえば、あるStoreで、2つのイベントをまちあわせて発火する、みたいな感じ? Fluxはまだよく調べてないので、要調査です。またReactは意図的にUIライブラリに特化しているので、組合せて使えるFRPライブラリがあるのかもしれません。

さらに一次イベントに限れば、立派なFRPな動作をしますが、それでは十分ではない、という考えです。

ReactはFPか?

私はYesだと思います。Reactの思想と実装はFP(関数型プログラミング(スタイル))に基いています。

Reactのコンポーネントのrenderは以下のような純粋関数です。

入力 出力
this.props, this.state React DOM, this.state

おいおいまってくださいよ、propsはともかく、stateは変更するんだから純粋ではない、と思うかもしれません。しかし、setStateは、それは実際の変更ではなく、新しい値なので、それを出力として返していると見ることができるのです。

stateを持つにせよもたないにせよ、renderは純粋関数として以下のように考えることができます。

引数:(props, state) -> 返り値:(React DOM, state')

ここでの、stateは、Reactコンポーネント1つのstateではなく、ReactDOM.render()全体で構築しようとするReact DOMツリー全体のstateを総合したものと考えてください(setStateがマージしていると考える)。

すると、おお、このstateとstate'を見ると、まさしくアレではないか。Stateモナド*2としてstateをもちまわり、renderとrenderが数珠繋ぎになって、貼り合さった全体のReact DOMが返却されるようにすれば、関数型としか言いようがありません。

つまり、Reactコンポーネントは、render以外を無視すれば、モナディックに合成され得る関数呼び出しを表現しています。render以外のメソッドは、これはただの関数であって、コンポーネントのクラスは関数置き場でしかありません。DOMのイベントハンドラに登録したり、他のコンポーネントのイベントをコールバックするのに用います。

FPとOOPの真の関係

そうだとしても、先のFRPの論法にしたがえば、FPとしての見た目、抽象があってこそのFPなのではないか? オブジェクト指向の部品を使って実現したものがFPと言えるのか、と思うかもしれません。

でも、FPというのは「値を変更しない」という制約にすぎません。「何かをしない」という制約は、FPを想定していない言語でも、BASICでも実現可能です。たとえば変数に再代入しないように注意したり、破壊的操作のないライブラリを使用すれば良いでしょう。だから、OOPでFPが実現できて何の不思議もない。

そしてそれは、varを使わずconstを使えといったミクロなレベルの話だけでなく、OOPの部品(クラス、オブジェクトインスタンス)を注意深く選択配置すれば、構造としてFPの思想をOOP上で実現でき、場合によってはFP専門の言語による実現よりもわかりやすくなる場合すらあるのだ、と今では考えています。

なお、言わずもがなですが、Reactの利用者は、FPがどうのとか認識する必要はありません。単に使って、便利にあらわれてきている特徴を享受すれば良いです。

まとめ

  • Reactは(単体では少なくとも)厳密にはFRPではない(一次イベントに限ればFRPと言えるかも)
  • ReactはFPである
  • Reactはいいね

*1:モジュールとして管理できるか?

*2:renderでは順序性の保証は重要ではなく、重要なのはStateの分離で、このことがReactにおけるタイムトラベリングデバッグとかヒストリ、リロード耐性、ホットスワップなどの実現に根幹的に寄与している。

広告を非表示にする