uehaj's blog

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

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

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

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

以下がReactでのマウスストーカーの実装です。Resultsで実行できます。

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

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を置くという方針
- flux
Singalの合成や加工操作(Signal.sampleOn, Time.delay,..) なし

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

なので差異の方が自分としては興味深いところです。たとえば、Elmでは純粋関数しかないので「直接Signalを発行できない(setStateできない)」という制限が大きく、Reactではハンドラ内のsetState()で済むところをport経由でTaskを起動したりしないとなりません(MessageやAddressを経由した暗黙にせよ)。

また、ElmではTaskの定義場所と、そのタスクをキックするハンドラなどがポートを介してばらばらになりますが*1、両者を一つのクラスとして書いて対応付けるReactの方が私にはわかりやすく感じました。

ReactはFRPか?

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

とすると、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」で、Time varring valueを操作したものをTime varring valueとして生成しています。

かたや、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で自分を追跡する★の位置を指定時間遅延させて発火させています。JSでの泥臭いイベントハンドラの定義と呼び出しの処理です。

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

とはいえ、一次イベントについては、renderから見たstateは透過的にアクセスでき、限定的にはFRPを実現しているとも言えます。先の例でいうmousemoveイベントで取得できるマウス位置は、刻々とかわるTime Varring Valueですが、renderの中では通常の値のようにあつかえているでしょう。二次イベントは、手作業でがんばってstateに登録することになります。

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

まとめると、Reactは単体では定義上のFPRではありません。しかし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のイベントハンドラに登録したり、他のコンポーネントのイベントをコールバックするのに用います。

(2015/11/08)上記はまちがっている。renderが純粋、いうのは正しいが、stateを変更するのはイベントハンドラの流れなのでrenderではもともとstateを更新も新規作成もしない。

FPとOOPの真の関係

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

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

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

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

まとめ

  • Reactは(単体では少なくとも)FRPではない。二次イベントのTime Varring Valueとしての構成や、Time Varring Valueの抽象操作を一般にサポートしていないため。ただし、一次イベントについては、renderから透過的にアクセスでき、FRPが提供する利点を得ることができる。
  • ReactはFPである
  • Reactはいいね

追記

こちらの記事によれば、

React が Reactive プログラミングを採用しなかった理由
React.js の開発者で Facebook につとめる Sebastian 氏は Staltz 氏の開発する Cycle.js を賞賛しながらも、Reactive プログラミングのスタイルを採用しなかった理由として、大多数の人が学ぶには大変であることを述べています (Hacker News)。

とのことです。

*1:モジュール・パッケージとしてまとめられるか?

*2:Stateモナドとしてみると、順序性の担保は重要ではない。ReactにとってはStateを切り離すことによって、ホットリロード、ヒストリ、タイムトラベリングデバッグ、リロード耐性などが実現されることが重要。