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

Arantium Maestum

プログラミング、囲碁、読書の話題

Clojure Web Development勉強 - ReactチュートリアルをReagentでやってみる(その3)

Clojure Clojurescript Web React Reagent

公式チュートリアルのFunctional ComponentsからDeclaring a Winnerまで。

Functional Components

前回散々JavaScript版が明示的じゃない、ボイラープレートが多いと煽っていたが、実際にはJSでも純粋関数的に書くとより簡潔になる:

function Square(props) {
  return (
    <button className="square" onClick={() => props.onClick()}>
      {props.value}
    </button>
  );
}

これまで見てきたReactコンポーネントはオブジェクト(クラス)だったが、上記のコンポーネントは関数として定義されている。このFunctional ComponentsがForm 1 Reagent Componentと同等のものになる。関数なのでデータを保持せず、thisを持たない代わりに明示的にpropsを引数で受け取っている。欲をいえばpropsの中のフィールドを関数の中で参照するんじゃなくてdestructuring的な何かで引数を受け取る段階でpropsの何を使うか明示してほしいが、それはまあそこまで重要なポイントではない。

とりあえずClojureScript版も再掲:

(defn Square [value click-fn]
  [:button.square {:on-click click-fn} value])

Taking Turns

二プレーヤーゲームなので、Xの次はO、Oの次はXと手番が入れ替わらないといけない。

Boardコンポーネントに変更を加えて実装する。

Javascriptだとこんな感じ:

class Board extends React.Component {
  constructor() {
    super();
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true,
    };
  }
  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }
  ...
}

BoardコンポーネントにxIsNextというプロパティをつけ、handleClick関数でそのプロパティの状態によってXかOをsquaresの正しい箇所(クリックされた四角の位置に相当するindex)に入れ、そしてhandleClick関数の中でxIsNextの真偽を入れ替える。

ClojureScript版も非常に似ている:

(defn Board []
  (let [squares (r/atom (apply vector (repeat 9 nil)))
        xIsNext (r/atom true)
        handle-click #(do (swap! squares assoc % (if @xIsNext "X" "O"))
                          (swap! xIsNext not))
        ...]
    ...))

xIsNextをreagent atomとして定義、handle-clickでその値によってsquareにXを入れるかOを入れるか決め、最後にxIsNextの値を反転させている。

Declaring a Winner

ゲームとして最低限成り立たせる最後の一歩として、勝者判定をしないといけない。

○×ゲームなので、三つ同じシンボルが縦横斜めのいずれかに並んだらそのプレーヤーの勝ちである。

まずは判定用のcalculateWinner関数を定義。

JavaScript版:

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

勝利条件のindexのセットをリスト化して、そのリストをループしてsquaresの三つ並びなlineの中で要素がnullじゃなく全部同一なものを探し、そのlineの第一要素を返す。ループで見つからなければnullを返す、というよくある命令的ループ。

ClojureScript版:

(defn calculateWinner [squares]
  (let [lines [[0 1 2]
               [3 4 5]
               [6 7 8]
               [0 3 6]
               [1 4 7]
               [2 5 8]
               [0 4 8]
               [2 4 6]]]
    (->> lines
         (filter (fn [[a b c]]
                   (and
                     (not (nil? (get squares a)))
                     (= (get squares a)
                        (get squares b)
                        (get squares c)))))
         first
         first
         (get squares))))
  • linesという名前のローカル変数で勝利条件となるindexの並びを定義
  • threading-macroでこのlinesからはじめてWinnerのシンボルかnilを返すまで処理をつなげている
  • linesの中の要素(便宜上lineと呼ぶ)を以下の条件でフィルタ
    • 最初のindexの位置にあるsquaresの要素がnil以外
    • lineの要素a b cのすべての位置にあるsquaresの要素が同じシンボル
  • その条件にマッチしている最初のlineだけを選択
  • そのlineの第一要素(フィルタがかかっているので第二、第三要素も同一のはず)を選択
  • その第一要素をindexとしてsquaresから値を取ってくる

ちなみにフィルタにマッチする条件がない場合nilが返ってきて、それ以降のステップでも引数がnilだと戻り値もnilになる。

JavaScript版に比べてちょっと行数が長くなっているが、主にfilterに渡す関数のところで条件を別の行にわけているのが大きい。

あと本当はgetいらないんだけど、どうだろう、あったほうが個人的には好き。squaresという名前に束縛されているデータ構造を関数として扱うのはちょっと抵抗がある。

まあないほうがずっと簡潔になるよな。ただし(squares nil)ってやると第一要素が帰ってくるので気をつけないといけないけど・・・

この実装だとJavaScript版を結構忠実に真似ているけど、linesを手で書くよりいい方法が絶対ありそうなものだ。

次に、このcalculateWinnerを使ってコンポーネントの挙動を変更する。具体的には、勝者がいる場合は「次の手番のプレーヤー」の代わりに「勝者」を表示し、その後ボードをクリックしても何も起こらないようにする。

JavaScript版:

class Board extends React.Component {
  ...
  handleClick(i) {
    const squares = this.state.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    ...
  }
  render() {
    const winner = calculateWinner(this.state.squares);
    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }
    ...
  }
}

onClickではcalculateWinnerがnull以外を返すか、クリックされた箱にすでに値が表示されている場合はすぐにreturnするようになった。

またrender関数ではwinnerの有無によってstatus変数(以前はプロパティだと思ったのだがよく見たらただの変数だった)を作成して表示している。

ClojureScript版:

(defn Board []
  (let [squares (r/atom (apply vector (repeat 9 nil)))
        xIsNext (r/atom true)
        handle-click #(if-not (or (calculateWinner @squares) (@squares %))
                          (do (swap! squares assoc % (if @xIsNext "X" "O"))
                              (swap! xIsNext not)))
        renderSquare (fn [i] 
                       [Square (get @squares i) #(handle-click i)])]
    (fn []
      [:div
       [:div.status (if-some [winner (calculateWinner @squares)]
                      (str "Winner: " winner)
                      (str "Next player: " (if @xIsNext "X" "O")))]
        ...]])))

handle-clickではif-notを使って条件が合わない場合のみ処理を実行、条件があったらnilを返すようにしてある。

そしてとりあえずstatusを完全に消してしまった。render関数の中で直接文字列を[:div.status]に渡している。if-someでcalculateWinnerがnil以外を返した場合はwinner変数に束縛した上で(str "Winner: " winner)を表示し、calculateWinnerがnilな場合は(str "Next player: " (if @xIsNext "X" "O"))を表示する。

if-notやif-someみたいな小技はまだなかなか自然には出てきてくれないが、「あ、このコードであれ使えるんじゃ?」とふと気づくと嬉しい。

ちなみにcalculateWinnerはBoardの前に宣言、r/renderの前に定義しておかないと呼び出すものがないと怒られるので注意。JavaScriptは一番最後にcalculateWinnerが来ていたが、言語仕様としてコードの宣言・定義の順番には無頓着なのか?

今回のコードでとりあえずゲームとしての体裁は整ってきた。次回でとりあえずReactチュートリアルは終わり。過去の手順の表示など、不特定数のDOM要素を動的に作る時の注意点などがポイント。

今回のコード:

gist.github.com

Clojure Web Development勉強 - ReactチュートリアルをReagentでやってみる(その2)

Clojure Clojurescript Web React Reagent

React公式チュートリアルを進める。今回はPassing Data Through PropsからLifting State Upまで。

Passing Data Through Props

https://facebook.github.io/react/tutorial/tutorial.html#passing-data-through-props

コンポーネント(つまり呼び出し側のコンポーネント)から子コンポーネントにデータを渡す方法について。

ReactというかJSXだと、<Square />の中にx={y}のような代入式を入れることによってSquareコンポーネントにデータを送る:

class Board extends React.Component {
  renderSquare(i) {
    return <Square value={i} />;
  }
...
}

Reagentだと[Square]のようなVectorの第二要素以降(例えば[Square y])にするだけでよい:

(defn Board []
  (let [renderSquare (fn [i] [Square i])
        ...]
    ...))

Reactの場合、受けとる側の子コンポーネントは、渡されたデータにthis.propsというオブジェクトを通じてアクセスできる:

class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {this.props.value}
      </button>
    );
  }
}

Reagentだと、明示的に引数として定義して、その引数名でアクセスする:

(defn Square [i]
  [:button.square i])

これでとりあえず0〜8までの数字が順番にSquareに表示されるようになっている。

個人的にはReagentの明示的な引数という方式は非常に好ましい。JavaScriptの方式だと、親コンポーネントthis.propsというオブジェクトにどんな名前のプロパティでもつけられ、子コンポーネントも何のプロパティを使ってもよく、子コンポーネントがどこで何のプロパティをどんな名前で使っているかを把握した上で親コンポーネントを書かなければいけなく、Zen of Pythonじゃないけど"explicit is better than implicit"と言いたくなる。

あといちいち{this.props.value}とかvalue={i}とか書かなくていいのも嬉しい。

An Interactive Component

https://facebook.github.io/react/tutorial/tutorial.html#an-interactive-component

クリックに反応する挙動を実装していく。

まずはクリックするとJavaScriptのポップアップアラートが開くような機能を試しにつけてみる。

JavaScriptだとonClickプロパティに匿名関数をくっつける:

<button className="square" onClick={() => alert('click')}>

Reagentでも似たようなもの:

[:button.square {:on-click #(js/alert "click")}]

これで「クリックするとアラート」という機能は実装できた。

次は「クリックすると×マークがつく」という機能。Squareコンポーネントに状態を持たせてみる:

class Square extends React.Component {
  constructor() {
    super();
    this.state = {
      value: null,
    };
  }
  render() {
    return (
      <button className="square" onClick={() => this.setState({value: 'X'})}>
          {this.state.value}
      </button>
    );
  }
}

Squareコンポーネントにコンストラクタを定義し、そこでthis.state.valueをnullに初期化。その値を常に表示するようにし、クリックイベントでthis.state.valueの値を"X"に変更する。変更された時点で表示も正しく更新される。

Clojureだとこう:

(defn Square [i]
  (let [state (r/atom nil)]
    (fn [i]
      [:button.square {:on-click #(reset! state "X")}
       @state])))

this.state.valueではなく、コンポーネントクロージャで束縛されているstate atomにデータを保持させる。クリックで更新、常に最新の値を表示させる、というのはJavaScript版とまったく同じ。

これでSquareの一つをクリックするとそこに×マークがつく。

Lifting State Up

https://facebook.github.io/react/tutorial/tutorial.html#lifting-state-up

Squareに状態持たせると勝ち負け判定とかできないよね、状態はすべてBoardに引き上げてしまってSquareはステートレスに戻そう、という方向で実装していく。

まずBoardコンポーネント側のJavaScript

class Board extends React.Component {
  constructor() {
    super();
    this.state = {
      squares: Array(9).fill(null),
    };
  }
  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = 'X';
    this.setState({squares: squares});
  }
  renderSquare(i) {
    return <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} />;
  }
}

this.state.squaresという状態を持たせるためのコンストラクタと、その状態をアップデートするためのhandleClickメソッド関数と、this.state.squaresの一部の値とその一部を変更できるhandleClick関数をSquareのthis.propsに渡すrenderSquareメソッド関数を定義している。

Clojureではすべてletで定義・束縛していく:

(defn Board []
  (let [squares (r/atom (apply vector (repeat 9 nil)))
        handle-click #(swap! squares assoc % "X")
        renderSquare (fn [i] 
                       [Square (get @squares i) #(handle-click i)])
        ...]
    ...))

squaresはreagent atom。handle-clickはそのatomクロージャに持ち、変更できる関数。renderSquareでSquareに引数としてsquaresの中のi番目の値と、#(handle-click i)という匿名関数を渡してやる。

Squareコンポーネントthis.stateを消し、データはthis.propsからとるようにする。this.props.onClickの中身がBoardコンポーネントのメソッドなおかげでBoardのthis.state.squaresを変更できる。

class Square extends React.Component {
  render() {
    return (
      <button className="square"  onClick={() => this.props.onClick()}>
        {this.props.value}
      </button>
    );
  }
}

Reagentで実装するとこんな感じ:

(defn Square [value click-fn]
  [:button.square {:on-click click-fn} value])

明示的かつ簡潔。

挙動は「An Interactive Component」で実装したものと同一。ただし、squaresがBoardレベルで定義されていることで、これをチェックすれば勝ち負け判定などができるようになった。

ここまでの感想

JavaScriptのほうはボイラープレートの多さもさることながら、暗黙のうちに定義されているものが多い印象。

例えばthis.propsとかその中のフィールドとかは唐突に「存在するもの」として子コンポーネントのコードで使われる。あとthis.setStateとかも定義されずに出現する。

ボイラープレートが減るのはいいことなのだけど、同じ命令的オブジェクト指向言語でもPythonに慣れ親しんだ身としては「明示的であることは犠牲にしないでくれ!」と言いたくなる。慣れの問題も大きいのだろうけど。

そういう意味では、ClojurePythonとの文化的・思想的な距離は意外と近い(ところもある)のかもしれない。

次回もチュートリアルを進めていく。

今回の終わりの時点でのコード(core.cljsのみ。他のファイルは変更なし):

gist.github.com

Clojure Web Development勉強 - ReactチュートリアルをReagentでやってみる(その1)

Clojure Clojurescript Web React Reagent

Reactの公式ページに基本概念などの説明のためのチュートリアルがある:

facebook.github.io

これをClojureScript/Reagentでやっていきたい。

最終的な成果物は簡単な○×ゲーム

HTMLやCSS、そしてReact Componentの大枠はすでに出来上がっていて、チュートリアルではこれに肉付けしていく流れとなっている。

codepen.io

今回とりあえずこの枠組みだけClojureScript/Reagentで用意した

CSSだけチュートリアルのサンプルコードの物をそのまま写して、それ以外はClojureScript的にアレンジしてある。HTMLはチュートリアルのものがなんだかJavaScriptでごちゃごちゃしていたのだが、今のところはあらかた無視してCSSJavaScriptのインポートとReagent用のDivの設定だけ。project.cljはいつもの。

さて、肝はcore.cljsである。その中にSquare、BoardそしてGameという三つのReactコンポーネントが定義されている。ClojureScriptとJavaScriptを対比させながら見ていこう。

Squareコンポーネント

SquareはこのReact Appの最小コンポーネントで、クリックされると○か×を表示する小さな四角い枠である。

ClojureScript:

(defn Square []
  [:button.square])

JavaScript:

class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {/* TODO */}
      </button>
    );
  }
}

ミジカァァァいッ!説明不要!

というのも寂しいので少し解説すると、Squareコンポーネントcssで定義されたsquare classのbutton要素である。今のところ引数なし。Reagentではclassは[:div.element-class]、idは[:div#element-id]と書ける。

特にこのような現状ではものすごくシンプルなコンポーネントについては、JavaScript/Reactに比べてボイラープレートが非常に少ない。というかこれ以上切り詰められないのではと思わせるほどの素晴らしいシグナル・ノイズ比。

Boardコンポーネント

次のプレーヤーが誰かを書いた:divとSquareコンポーネントを9つ表示するBoardコンポーネント

ClojureScript:

(defn Board []
  (let [renderSquare (fn [i] [Square])
        status (r/atom "Next player: X")]
    (fn []
      [:div
       [:div.status @status]
       [:div.board-row
        [renderSquare 0]
        [renderSquare 1]
        [renderSquare 2]]
       [:div.board-row
        [renderSquare 3]
        [renderSquare 4]
        [renderSquare 5]]
       [:div.board-row
        [renderSquare 6]
        [renderSquare 7]
        [renderSquare 8]]])))

JavaScript:

class Board extends React.Component {
  renderSquare(i) {
    return <Square />;
  }
  render() {
    const status = 'Next player: X';
    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

コンポーネント固有のrenderSquare methodとstatusという文字列が存在する。ClojureScript版ではletを使ってRe-FrameのいうところのForm-2 Componentを作成し、クロージャでrenderSquareとstatusをコンポーネントに束縛している。

renderSquareは現在まったく無駄だが(Squareを直接呼び出せば良い)、チュートリアルを先読みすると、後々Squareに渡す引数の作成など複雑な処理が入る予定っぽい。面白いことに、ClojureScript版だとrenderSquare自体がReagent的にはForm-2 Component関数になっている(はず)。

そのおかげもあり、非常に統一された外見で何が起きているかある程度わかりやすいように感じる。JavaScriptのほうは本当にJavaScriptとHTMLの悪魔合体というかなんというか・・・ ただ、実際の処理に関してはClojureScript版のほうが「どの時点でどの処理が実行されるか」(特にlet bindingが再実行されてしまうかどうか)に気をつかわないといけないところはあるかもしれない。

微妙に[:div.board-row ...]あたりを別コンポーネントに切り出したくなるが、とりあえず今は我慢・・・

Gameコンポーネント

Boardとゲーム情報をリストとして表示するdivを持ったGameコンポーネント

(defn Game []
  [:div.game
   [:div.game-board
    [Board]]
   [:div.game-info
    [:div]
    [:ol]]])
class Game extends React.Component {
  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

あまり話すことがない。他の二つに比べるとJavaScript/ClojureScriptの対比もあまり際立っていない。

calculateWinner関数はReactと関係ないので実際に使われる段階になって解説する。

次回以降チュートリアル通りに進めていく。

今回のソース:

gist.github.com

Clojure Web Development勉強 - Reagent(その3)状態の管理

Clojure Clojurescript Web React Reagent

Reagentでは、Hiccup構文を使ったコンポーネント関数とReagent Atomを使って、複雑な状態を持つクライアントサイド・アプリケーションを作成できる。

前回の例ではコードをなるべく単純に留めるために、トップレベルでReagent Atomを定義し、各コンポーネントはそのAtomを直接参照していた。基本的なモデルは古き悪きグローバル変数である。

グローバル変数の弊害について多くを語る必要はさほどないとは思うが、一つすぐにぶつかる問題として、コンポーネント関数のモジュール性が著しく損なわれてしまうことが挙げられる。

前回のコードを見てみると:

(def clicks 
  (r/atom {:John 0 :James 0}))

(defn clicker [name-key]
  [:div
    [:button {:on-click #(swap! clicks update name-key inc)} name-key]
    [:p "Click count: " (get @clicks name-key)]])

(defn my-app []
  [:div
    [clicker :John]
    [clicker :James]])

clickerコンポーネント関数がname-keyだけを引数にしていて、clicks atomはトップレベル定義のものを直接参照している。

clickerコンポーネントがclicksという名前に依存しているというのも、clickerの再利用性を低くしコード全体の変更をめんどくさくする要因となり得る。が、まあそれはそれなりのエディタを使っていればなんとか・・・

グローバルなatomのせいでテスト性が低下しているのは痛い。

そして何より、clicks(というかアプリケーションのグローバル・ステートatom)の構造とコンポーネントのロジックが分離できていないのが最大のポイントとなる。どういうことかというと、例えば上記のコードでmy-appとしている部分を別のコンポーネントとして切り出して、ステートatom

(def app-state 
  (r/atom {:names {:John "John Doe" :James "James Bond"}
           :clicks {:John 0 :James 0}))

のように変えたとすると、最末端の枝部分になるclicker関数のコードを変更しなくてはいけなくなる。そしてその後も変更が加わる事に、すべてのコンポーネントをアップデートする羽目になる。

こういう状況を回避するためにうまくreagent atomを使うパターンがいくつか存在している。

引数に値(状態の表示のみ)

コンポーネントが値を表示するだけなら非常に簡単。atomをまったく使わずに、引数にコンポーネントの状態を渡してやってそれで終わり。

(def app-state 
  (r/atom {:names {:John "John Doe" :James "James Bond"}
           :clicks {:John 0 :James 0}))

(defn namer [name-string]
  [:h1 name-string])

(defn names [name-state]
  [:div
    [namer (get name-state :John)]
    [namer (get name-state :James)]])

(defn my-app []
  [:div
    [names (get @app-state :names)]])

これだとnamer関数はデータ構造の知識はまったく入っておらず、単に文字列を受け取って表示しているだけ。names関数は自分が担当する名前の入ったimmutableなmapを引数にしていて、それ以外のapp-stateについての情報はまったくない。app-stateの構造がどう変わろうが、namesとnamerが必要な部分がちゃんとあれば、これら枝部分の関数は変更なしで済む。

ただし、これはデータを単純に受けとって表示するようなコンポーネントでしか使えない手だ。clickerのように、コンポーネントに対するユーザの行動から状態を更新していくようなコンポーネントを書く場合、状態atomになんらかのアクセスが必要になる。

Cursor (状態の表示と変更)

そのためのメカニズムがcursorである。これはもともとはOmにあったのをReagentにも導入したもののようだ。

状態atomを細分化し、参照・変更どちらも可能な新しいatom(的なもの)を作り出してくれる。実際にはatomとはちょっと違うのだが、同じAPIで使えて、元のatomと参照・変更の双方向でつながっている。

(def my-data (r/atom {:a 0}))
(def a (r/cursor my-data [:a]))
(js/console.log @a)

(swap! my-data update :a inc)
(js/console.log @a)

(swap! a inc)
(js/console.log @my-data)

これでブラウザのログに0と1と{:a 2}が表示される。my-dataの変更が自動的にaの値を更新し、aの変更が自動的にmy-dataの値を更新していることがわかる。

これをreagent的に使ってみると:

(def app-state 
  (r/atom {:names {:John "John Doe" :James "James Bond"}
           :clicks {:John 0 :James 0}}))

(defn namer [name-string]
  [:h1 name-string])

(defn clicker [click-atom]
  [:div
    [:button {:on-click #(swap! click-atom inc)} "Click me"]
    [:p "Click count: " @click-atom]])

(defn name-and-click [name-key names-map clicks-atom]
  [:div
    [namer (get names-map name-key)]
    [clicker (r/cursor clicks-atom [name-key])]])

(defn my-app []
  [:div
    [name-and-click :John (get @app-state :names) (r/cursor app-state [:clicks])]
    [name-and-click :James (get @app-state :names) (r/cursor app-state [:clicks])]])

(普通はmy-app内がごちゃごちゃしているのをletで変数束縛してスッキリさせるが、reagentコンポーネント内でのletは少し特殊でまだ説明していないので割愛)

上記のコードではname-and-clickコンポーネント

  • immutableなnames-map
  • cursorで細分化されたclicks-atom
  • 両方のkeyとなるname-key

を渡していて、そこからさらに細分化して下位コンポーネントのclickerにclicks-atomをcursorを使って細分化したclick-atomを、そしてnamerには単なる文字列を渡している。

これで下位のコンポーネントからは全体のデータ構造を隠蔽して必要な部分だけの参照・変更ができる。

コンポーネントレベルでの状態保持

最後に、トップレベルではなく、コンポーネントが自分の状態をClosureを使って保持することも可能だ。

clickのデータをClosureで保持するとしたらこのようになる:

(defn clicker [name-string]
  (let [clicks (r/atom 0)]
    (fn [name-string]
      [:div
        [:button {:on-click #(swap! clicks inc)} name-string]
        [:p "Click count: " @clicks]])))

(defn my-app []
  [:div
    [clicker "John"]
    [clicker "James"]])

注目するべきポイントは二つ。

  • letの中にfnが入っている
  • コードが非常に簡単になっている

まず第一のポイントについてはRe-FrameのComponent Tutorialに詳しい。

外側のclicker関数は「コンポーネントが作成された」一回のみ実行され、clicksのr/atomを作成し、そしてそのclicksがローカルクロージャで束縛されている内側の無名関数を返す。その後clicksが変更され、レンダし直す必要があるたびに実行されるのはこの内側の匿名関数である。

この内側の匿名関数を作らずに

(defn clicker [name-string]
  (let [clicks (r/atom 0)]
    [:div
      [:button {:on-click #(swap! clicks inc)} name-string]
      [:p "Click count: " @clicks]]))

のようにしてしまうと、ボタンをクリックしても数字が変わらない。なぜならclicks r/atomが変更されるたび、clickerが実行され、0の値の新しいclicks r/atomが作成されてその値が表示されるからである。

第二のポイントは実に甘美な誘惑で、状態をコンポーネント自身に管理させると非常に綺麗かつ簡潔に書ける。

ただし、これには大きな陥穽が二つある。

外部から参照も変更もできない

例えばアプリケーションの他の部分でこのクリック数を使おうと思ってもそのデータはローカルクロージャに隠蔽されていてアクセスできない。なのでこの手法は本当にそのコンポーネントだけで完結するデータでしか使えない

「アプリケーション全体の状態がデータによって一義的に決まる」という原則から外れてしまう

もし本当にコンポーネント固有のデータだったとしても、ローカルクロージャに入ったデータは「外部から変更もできない」ので、その状態を再現することが難しくなる。ReactとClojureScriptの大きな思想的売りである「データによる一義的に決定される関数型・宣言的プログラミング的フロントエンド開発」から離れてしまう。これは実際のところコストは大きい。

以上の二点を踏まえて、この手法は理想としては「マウスが現在ボタンの上にあるかどうかなど、アプリケーションの状態というにはささやかすぎることを保持するのには利用するべき」とされることが多い。ただし実際にはローカルステートは非常に多用される。楽だから。

今回は「Reagentでアプリケーションの状態をどう管理するのか」について簡単に見てきた。

次は簡単なReactのサンプルコードをReagentに移植してみたい。

Clojure Web Development勉強 - Reagent(その2)Reagent Atom

Clojure Clojurescript Web React Reagent

さて、Reagentの二大要素の二つ目であるReagent Atomについて。

Reagentでは、コンポーネントの可変な状態を保持するために特殊なAtomを用意している。

と言っても概念・機能は通常のClojureAtomとほとんど同じで、Immutableなデータ構造へのポインタのようなものである。

Clojure Atomと同じInterfaceを持ち、swap!やreset!などで新しいデータを指すようにでき、その指し示すデータを@でderefできる。

例えば:

(def a (r/atom 0))
(js/console.log @a)

(reset! a 5)
(js/console.log @a)

(swap! a inc)
(js/console.log @a)

これでブラウザのログに0と5と6が出力される。

ではReagent AtomClojure Atomとの違いは何か。公式のReagentチュートリアルによると:

The easiest way to manage state in Reagent is to use Reagent’s own version of atom. It works exactly like the one in clojure.core, except that it keeps track of every time it is deref’ed. Any component that uses an atom is automagically re-rendered when its value changes.

つまり、あるReagent Atomを参照しているコンポーネントはそのAtomの値が変わるたびに自動的に更新され、常に最新の値を反映しているようになる。

例:

(def clicks (r/atom 0))

(defn clicker []
  [:div
    [:button {:on-click #(swap! clicks inc)} "Click me"]
    [:p "Click count: " @clicks]])

最初のbutton要素はclickイベントに反応して#(swap! clicks inc)が走るようになっており、clicksのその時その時の値が、p要素に表示されるようになっている。

Reagent Atomの中身はもちろんVectorやMapであっても良い:

(def clicks 
  (r/atom {:John 0 :James 0}))

(defn clicker [name-key]
  [:div
    [:button {:on-click #(swap! clicks update name-key inc)} name-key]
    [:p "Click count: " (get @clicks name-key)]])

(defn my-app []
  [:div
    [clicker :John]
    [clicker :James]])

clickerコンポーネント関数がkeyを受け取り、そのkeyに応じてclick r/atomのmapに入っている正しい値を表示したり更新したりしている。:button内での名前の表示は、Reagentの0.6.0-rc以降、keywordを直接コンポーネントの引数としていれると文字列として表示してくれるようになったのを利用している。

次回はこのr/atomを使ったコンポーネントの状態管理のパターンをいくつか紹介したい。

Clojure Web Development勉強 - Reagent(その1)概要とHiccup構文

Clojure Clojurescript Web React Reagent

ReactのClojureScript WrapperであるReagentライブラリについて。

前回書いたとおり、ClojureScriptには有名なReact Wrapperが三つある。Om/Om-Next、Quiescent、そしてこのReagentである。

最初の二つの特徴を非常にざっくりと要約するなら:

  • Om/Om-Nextは精緻、強力、先進かつ複雑
  • QuiescentはClojureScriptでReactを使うため必要最低限の機能を備えた簡単かつ自由度の高いデザイン

となる。

それに対して、Reagentは

Reactの思想を汲みつつ「VectorとMapをベースにしたHiccup的DSL」と「Clojure Atomの機能を拡張したReagent Atom」の二要素を中心として極力シンプルかつClojure的に再構築したライブラリ

である。この「シンプルかつClojure的」というのがとても大事で、個人的には、習得しやすく、書き始めると自然に感じられ、書いていて気持ちが良い、と非常に好印象である。今のところClojureScriptを書く場合ほとんどReagentを使っており、少し知見もたまってきたので、他のライブラリよりも詳細に踏み込んで書いていく。

今回は第一要素である「VectorとMapをベースにしたHiccup的DSL」の話をしたい。

HTMLコードの解説

と、その前にReact/Reagentのお約束となるHTMLドキュメントのレイアウトについて一言。

大体こんな感じになるかと思う。

<html>
    <body>
        <div id="appdiv"></div>

        <script src="js/out/goog/base.js"></script>
        <script src="js/main.js"></script>
        <script>goog.require('min_reagent.core')</script>
    </body>
</html>

<script>の部分は毎度おなじみだが、その前に<div id="appdiv"></div>という新要素が追加されている。このappdivという名前が付いているDOM要素をReagentに渡してやると、Reagentがその中にどんどんDOM要素を詰め込んでページを構築してくれる。

appdivという名前は特に重要ではなく、あくまでHTMLとcljsファイルの双方で同じ名前が使われていれば良い。

<div id="appdiv"></div><script>の後に来ると、最初にロードされたreagentコードがDOM要素を見つけられずエラーを起こすので注意。必ず<div id="appdiv"></div>が先にくる必要がある。

ReagentのComponent関数

(defn my-app []
  [:div
    [:h1 "Hello Reagent"]
    [:p "ClojureScript + React + Atoms + Hiccup"]])

この時点で注意したいのは、このコンポーネント関数、あくまでVectorを返す純粋関数であって、副作用を起こすようなものはまったくない。何の変哲もないVectorなので様々なClojureのデータ構造操作関数で作ったりいじったりできる。

さらに、非常に簡単にコンポーネント化して組み合わせることができる。

(defn my-component []
  [:div
    [:h1 "Hello Reagent"]
    [:p "ClojureScript + React + Atoms + Hiccup"]])

(defn my-app []
  [:div
    [my-component]
    [my-component]])

先ほどまでmy-appという名前だった関数をmy-componentと名称変更し、my-appはそのmy-componentが二つ入っている:divにした。my-componentをmy-appから関数呼び出しするのではなく、関数自体をVectorに入れているのが注意ポイント。この例だとほとんど違いはないがより動的なコンポーネントの場合、このReagentの機能を使うための記述をする・しないで挙動や効率が大きく変わってくる。

コンポーネント関数はその名の通り関数なので、引数を取れる:

(defn my-component [name-str]
  [:div
    [:h1 "Hello " name-str]
    [:p "ClojureScript + React + Atoms + Hiccup"]])

(defn my-app []
  [:div
    [my-component "Reagent"]
    [my-component "Figwheel"]])

my-componentでname-strという引数をとり、h1内で表示する。my-appで[my-component "Reagent"]のように、関数呼び出しではなくあくまでVectorの第一要素がコンポーネント関数、第二要素(以降)が引数となる。

DOM要素のプロパティはMapで定義できる:

(defn my-component [name-str]
  [:div
    [:h1 {:on-click #(js/alert name-str)}
     "Hello " name-str]
    [:p "ClojureScript + React + Atoms + Hiccup"]])

(my-app関数は変化なし)

h1要素のon-clickプロパティに、JavaScriptのalertでname-strを出力する匿名関数を束縛した。これでh1要素をクリックするとアラートメッセージで"Reagent"や"Figwheel"と表示される。

と、ここまでの情報でHTMLのあらかたの要素とプロパティが使えてしまう。さらにfigwheelを使っていれば、いじって保存して、でグリグリとブラウザ上でページが変わっていくのでとても楽しい。ぜひお試しあれ。

ただし、コンポーネント関数自体は描画のロジックはまったく含まれていない。というか現時点では本当にただのデータ構造を返す関数である。

ReagentのRender関数

そのコンポーネント関数を受けとり、実際にブラウザ上で表示させるのを担当するのがreagentのrender関数である。

(r/render
  [my-app]
  (js/document.getElementById "appdiv"))

上記のコードでは、reagent.coreという名前空間をrに束縛してある。r/renderがreagentライブラリのrender関数である。

アプリケーショントップレベルのコンポーネント関数を含んだVectorと、document.getElementByIdというJavaScript関数を使ってアクセスした"appdiv"というidのDOM要素を引数にしている。この部分だけは純粋関数ではなくて「描画」という副作用を起こすものとなっている。

データ構造を返す純粋関数として作られているコンポーネントを組み合わせて、最終的に作り上げたトップレベコンポーネントを描画担当のrender関数に渡すのがここまでの流れ。次回はアプリケーションの状態を保持し、ステート変更をうまくComponent関数に伝達するReagent Atomの話。

今回のコード:

gist.github.com

Clojure Web Development勉強 - Reactあれこれ

React Clojurescript Clojure Web

Facebookが提供するJavaScriptライブラリであるReactについていろいろと書こうと思う。

まず、私自身がReactについて調べていた時に大変参考になった記事を三つ挙げる:

qiita.com

mizchi.hatenablog.com

qiita.com

本記事と前後してこれらも読めば、Reactについてはよく理解いただけるのではないか。

React前夜

今までのWebフロントエンドのモデルは

  1. HTMLで文書の構造を記述し
  2. CSSで見た目を整え
  3. JavaScriptでブラウザサイドの動的なロジックを加える

というもので、JQueryの出現などによって多少の動的挙動ならそれなりに記述しやすくなった。しかしブラウザ上でサービスを提供するビジネスの隆盛に伴い、デスクトップアプリケーションと同等の複雑な挙動をページリロードなしで実装するSingle Page Application(略してSPAーー昔はRich Internet Applicationとか言いましたね)が必要となってくる。

JQuery(あるいは生のJavaScriptで書くというさらなる地獄)ではこういった動的かつ柔軟なインターフェイスを実装するための基本的なモデルは「HTMLで記述された文書構造から立ち上がってくるDocument Object Modelの関連部分をJavaScriptによって逐次変更していく」というものだ。

その結果「グローバルなステートにローカルな変更を逐次与えていく」という命令型オブジェクト指向プログラミングで一番辛い流れになってしまう。

Reactモデル

Reactはその辛さを解消するための仕組みである。

HTML/CSS/JavaScriptの役割分担としては:

  1. HTMLには最低限のCSS+JavaScriptインポート、そしてReactが操作するためのフックとなる単一の空DOM要素を記述
  2. CSSは今までどおり見た目部分を記述
  3. JavaScriptで、引数によって必ず同一のDOM要素(あるいは要素のツリー)を返すコンポーネント関数を数多く記述
  4. 作ったコンポーネント関数を組み合わせてさらに大きなコンポーネント関数を作っていき、最終的にアプリケーション全体を表示するコンポーネントを作成
  5. ページの変遷はコンポーネントに与えられる引数が変わることで起きる

と、JavaScriptがHTMLを食ったような形である。実際JSXというJavaScriptの拡張言語で、より自然にHTMLタグをJSコードに埋め込んだ形式で記述することも可能になる。

HTMLで記述して描画したDOMをJavaScriptでいじるのではなく、すべてを「引数は『状態』、返り値は『DOM要素』」なJavaScriptの純粋関数で書いてしまって、状態変遷の度に単に新しい引数を渡して新しいDOMを作成してしまおう、というモデルである。

メリットとしては

  1. 引数によって描画されるものの同一性が担保されるので、アプリケーションのロジックについて考えやすくなる
  2. 同じ理由でテストが容易
  3. コンポーネント化によって再利用性が非常に高くなる

React自体からはちょっと離れるが、「Reactを使ったアプリケーションの作り方」であるFluxにおいてはさらに「Global Stateを一つのデータオブジェクトにすることで、ほとんどのロジックをそのデータに対する純粋関数として記述できる」という大きなメリットが生まれる。

しかし、このままでは状態変遷ごとに新しいDOMが作成されて描画されて、非常に効率が悪くなってしまう。DOMのローカルな部分をJavaScriptでいじるのは少なくとも「最低限のDOM変更で済む」というメリットがあったわけだ。

そこでReactの影の主役であるVirtual DOMという仕組みが生きてくる。このライブラリ側の仕組みが、「まるで新しいDOMを作成するかのように書かれたコード」をベースに「現在のDOMとあるべきDOMの差分」を算出し、その差分だけの「最低限のローカルなDOM変更のみ」を適用してくれる。宣言的に書いたコードが命令的にうまい具合に変換されるのである。

関数型言語コンパイラインタプリタが「泥臭い命令型の指示」を吸収して、プログラマにはそれらを意識することなく「純粋関数で書かれたコード」を書かせてくれるのとちょっと似てるかもしれない。

ReactとClojureScript

さて、ここまで書いたことを踏まえて考えていただくとわかると思うのだけれど、ReactとClojureScriptの相性は非常にいい。

いやむしろ

JQueryや生のJavaScriptのスタイルをそのまま使ってしまうと『DOMいじり』という非常に命令的でステートフルなモデルからしてClojureScriptの強さがまったく生きない

といった方が正解かもしれない。React的な方向にしろ別の方向(例えばHoplon)にしろ、どうにかしてその一般的なJavaScriptモデルから脱却せざるをえない。

逆にReactのような

DOMの初期構造も純粋関数の組み合わせで記述

状態変遷も本質的にはデータ構造として捉えられてるアプリケーション状態に純粋関数を適用して変換していくことで実施

というモデルではClojureScriptの効率のいいPersistent Immutable Data Structureと豊富かつ強力なデータ操作関数群が猛威を振るう。

現在比較的有名なClojureScript上のReact系ライブラリとしては

がある。

実際に私が使ったことがあるのはReagentのみ。他の二つに関して、今持っている知識と印象を書くと:

  • Om/Om-NextはClojureScriptのメインディベロッパーであるDavid Nolenのライブラリで、非常に精緻かつ先進的な機能を多く取り入れた強力なライブラリのようである。(Om-Nextは後方互換性のまったくない後継プロジェクト)ただしその分かなり独特で、Omの思想と技術的背景をある程度理解しないと使うのは難しい印象を受ける。
  • Quiescentは逆にReactをClojureScriptから使うための最小限のラッパーに近い。ただし、コンポーネントレベルで独自のステートを持つような機能がないので、Flux的な「トップレベルで一つの状態オブジェクトを持ち、そのデータが子コンポーネントに枝分かれして渡されていく」というモデルがデフォルトのようだ。

いつかこれらライブラリも是非使ってみたいものである。

Reagentの詳細は次回以降。