Arantium Maestum

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

Clojure Web Development勉強 - FigwheelでCSS Auto-Loading

Single Page Applicationで複雑かつ洗練されたGUIを実装するためには、一般的にHTMLで定義されている文書のコンテンツだけではなく、CSSで定義されているスタイルをどうコントロールするかも非常に重要なポイントになる。

JavaScriptで直接DOMにコンポーネントごとのスタイルを挿入することも可能なのだが、Reactの実例を見るとどちらかというとCSSは別途用意することの方が多いようである。コンポーネントのclassやid指定でCSSと紐付ける形になる。公式チュートリアルもこの方式を採っていた。

別にCSSファイルを用意するのはいいのだが、CSSの変更は普通HTMLをリロードしないと反映されない。せっかくFigwheelでClojureScriptの変更を自動的かつ即座に反映させているのに、コンポーネントの見た目をいじってもリロードしないと変化が見えないというのは片手落ちである。

幸いFigwheelには特定のディレクトリのCSSをモニタして、変更があればWeb Socketでオートロードしてくれる設定があるので使ってみる。

ディレクトリ構造

とりあえずこんな感じのディレクトリ構造

.
├── project.clj
├── resources
│   └── public
│       ├── css
│       │   └── main.css
│       └── index.html
└── src
    └── min_css
        └── core.cljs

いつもの構成に加えてresources/public/cssにmain.cssを定義している。

HTML

もちろんHTML文書でこのmain.cssを指定している:

<html>
    <head>
        <link href="css/main.css" rel="stylesheet" type="text/css">
    </head>
    <body>
        <div id="app"></div>
        
        <script src="js/out/goog/base.js"></script>
        <script src="js/main.js"></script>
        <script>goog.require('min_css.core')</script>
    </body>
</html>

あとは今までどおりにreagent用のapp div作成とJavaScriptの呼び出し。

Reagentコンポーネント

core.cljsの中にあるReagentのmy-appコンポーネントで要素を三つ定義している:

(defn my-app []
  [:div
   [:h1 "Hello Reagent"]
   [:p "Some random text in a regular p tag"]
   [:p.my-class "More random text in a my-class p tag"]])

h1とただのpとp.my-classである。Reactチュートリアルでも見たように、Reagentでは:tag.xでxクラス属性を、:tag#yでyというidをDOM要素に付与できる。

CSS

body全体とこの三つの要素のスタイルをCSSで定義する:

body {
    background: #ddd;
}

h1 {
    color: #f00;
}

p {
    font: 18px "Century Gothic", Futura, sans-serif;
}

.my-class {
    font-size: 12px;
    background: #ddf;
}

見た目はひどいがまあサンプルだと思って・・・

Figwheel設定

あとはproject.cljでfigwheelにcssの在処を指定してオートロードするよう設定する:

(defproject min-css "0.0.1"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [org.clojure/clojurescript "1.9.293"]
                 [reagent "0.6.0"]]

  :plugins [[lein-cljsbuild "1.1.4"]
            [lein-figwheel "0.5.7"]]

  :figwheel {:css-dirs ["resources/public/css"]} ;ここ

  :cljsbuild
  {:builds {:dev {:source-paths ["src"]
                  :figwheel true
                  :compiler {:output-to "resources/public/js/main.js"
                             :output-dir "resources/public/js/out/"
                             :optimizations :none}}}})

一行figwheelへの指示を追加するだけ。

実行

あとはlein figwheelで実行してブラウザでlocalhost:3449を開くだけ。

core.cljsもmain.cssも変更して保存した瞬間にブラウザで変更が反映される。

スタイルを動的に変更できるようになるのはけっこう快感。CSSシークレットみたいな本をいろいろとイジリ倒してみたくなる。

次はCSSの指定もednで!という業の深い話を。

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

ReactチュートリアルのShowing the Movesから最後のImplementing Time Travelまで。

Showing the Moves

https://facebook.github.io/react/tutorial/tutorial.html#showing-the-moves

前回作成したhistoryデータを使って過去の動きを表示するようにする。

JavaScript版:

class Game extends React.Component {
  ...
  render() {
    ...
    const moves = history.map((step, move) => {
      const desc = move ?
        'Move #' + move :
        'Game start';
      return (
        <li>
          <a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
        </li>
      );
    });

    return (
      <div className="game">
        ...
        <div className="game-info">
          <div>{status}</div>
          <ol>{moves}</ol>
        </div>
      </div>
    );
  }
}

historyのデータをもとにlistアイテムを動的に作って<ol>タグの中に入れている。各moveのlist itemをクリックするとJumpToという現在定義されていない関数が呼び出される。

実際にはこの段階だと「過去の動きの番号を並べる」だけで、実際にどういう手だったのかはわからない。その情報を表示するにはImplementing Time Travelまで待たないといけない。

ClojureScript版:

(defn Game []
  (let [...]
    (fn []
      (let [...
            jumpTo (fn [move] nil)
            move-desc #(if (zero? %) 
                        "Game start"
                        (str "Move #" %))
            move-elem (fn [move]
                        [:li 
                         [:a {:href "#" 
                             :onClick #(jumpTo move)}
                          (move-desc move)]])
            moves (->> @history
                       count
                       range
                       (map move-elem)
                       doall)]
        [:div.game
         ...
         [:div.game-info
          [:div status]
          [:ol moves]]]))))

JavaScriptと違ってClojureScriptでは定義されていない関数があるとコンパイルしないので、何もしないJumpTo関数を定義している。Implementing Time Travelでちゃんと内容も書く。

このコード、このままでも動くは動くのだが、Console Logを調べるとこんなワーニングが出ている:

react.inc.js:20209 Warning: Each child in an array or iterator should have a unique "key" prop.

Check the render method of react_tutorial.core.Game. See https://fb.me/react-warning-keys for more information.

in li (created by react_tutorial.core.Game)

in react_tutorial.core.Game

このワーニングを解決するために、コンポーネントにキーをつける必要がある。

Keys

https://facebook.github.io/react/tutorial/tutorial.html#keys

チュートリアルの説明によると:

key is a special property that's reserved by React (along with ref, a more advanced feature).

Keys tell React about the identity of each component, so that it can maintain the state across rerenders.

とのこと。キーをpropsっぽい形でコンポーネントに渡してやると、Reactが動的に作成されているコンポーネントの同一性を認識できるようになる。

JavaScript:

class Game extends React.Component {
  ...
  render() {
    ...
    const moves = history.map((step, move) => {
      ...
      return (
        <li key={move}>
          <a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
        </li>
      );
    });
    ...
  }
}

<li key={move}>というようにまるでpropsのように渡している。ちなみにこのkeyはあくまでこのコンポーネント内というコンテキストでの同一性判定に使われるので、まったく違うコンポーネントの中で同じキーが使われていても問題ない。

ClojureScript版:

(defn Game []
  (let [...]
    (fn []
      (let [...
            move-elem (fn [move]
                        [:li {:key move}
                         [:a {:href "#" 
                             :onClick #(jumpTo move)}
                          (move-desc move)]])
            ...]
        ...))))

ClojureScript/ReagentだとこのようにProps的に渡す方法と、^{:key move} [:li ...]というようにメタデータという形式でコンポーネントに紐付ける方法の2種類がある。今回はJavaScript/Reactと同じような見た目になるようPropsぽく渡している。

これでワーニングは消えるはずである。

Implementing Time Travel

https://facebook.github.io/react/tutorial/tutorial.html#implementing-time-travel

最後に、Showing the Movesで表示させた「過去の動き」をクリックするとゲームがその動きまで巻き戻る機能を実装する。

JavaScript:

class Game extends React.Component {
  constructor() {
    super();
    this.state = {
      ...,
      stepNumber: 0
    };
  }
  handleClick(i) {
    const history = this.state.history;
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      ...,
      stepNumber: history.length
    });
  }
  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) ? false : true,
    });
  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    ...
  }
}

Gameコンポーネントをかなり広範囲にわたっていじっている。

Gameに現在何手目かという情報を保持するstepNumberというstateフィールドを定義。それに合わせてhandleClickとrenderを変更し、jumpToでクリックされた「過去の動き」の番号にstepNumberを変更し、xIsNextもそれに合うように変更する。

上記のコードではmovesを消しているからわからないが、チュートリアルJavaScriptコードがすごくいけてない。jumpToの引数名はstepなのに、それを使っているmovesの中ではstepとmoveの二つの変数があってjumpToに渡しているのはmove。わかりにくい。

ClojureScript:

(defn Game []
  (let [...

        stepNumber (r/atom 0)
        
        handle-click
        (fn [i]
          (let [squares (:squares (get @history @stepNumber))]
             (if-not (or (calculateWinner squares) 
                         (squares i))
               (let [next-move (if @xIsNext "X" "O")
                     next-squares (assoc squares i next-move)
                     next-step {:squares next-squares}]
                 (do (reset! stepNumber (count @history))
                     (swap! history #(conj % next-step))
                     (swap! xIsNext not))))))

        jumpTo
        (fn [move]
          (do (reset! stepNumber move)
               (reset! xIsNext (zero? (mod move 2)))))]

    (fn []
      (let [squares (:squares (get @history @stepNumber))
            ...]
        ...))))

やってることはだいたいJavaScript版と同じ。

チュートリアルのこの章の最後のほうで

You may also want to update handleClick to be aware of stepNumber when reading the current board state so that you can go back in time then click in the board to create a new entry.

と書いてある。実際上記のコードを試すと、クリックして途中から新しい手を打つと、明らかにバグっぽい作動(同じプレーヤーが二回連続で打てたり)が出る。

「過去の動き」を表示している時にボードがクリックされると、その時点以降の「未来」データを上書きするようにhandleClickを修正する。

JavaScript:

class Game extends React.Component {
  ...
  handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber+1);
    ...
  }
  ...
}

けっこう簡単で、handleClickの冒頭でhistoryを「現在表示されているボードの状態」のところ以降を切り捨てるだけ。

ClojureScript版:

(defn Game []
  (let [...
        handle-click
        (fn [i]
          (swap! history #(apply vector (take (inc @stepNumber) %)))
          ...)))

JavaScriptと概念的にはまったく同じで、handle-clickの冒頭でhistory r/atomをstepNumberのところまでだけとって残りは捨てている。

これで途中まで戻って打ち出してもバグっぽい挙動は起きない。

まとめ

というわけでReactチュートリアルは一段落。

JavaScriptとClojureScriptで対応させるのは多くの場合は比較的簡単な印象。ちょこっと怪しいところもあるが・・・ (renderSquareが危険な香りがする)

なのでReactで書かれたコードをReagentにポートした上で修正したい、というような要望があってもそこまで難しくはないのではないか。

ただ、このチュートリアルのコードもそうだが、JavaScriptだとStateの扱いが雑になりがちな印象がある。結局Mutableなので、子コンポーネントに渡したStateを簡単に書き換えられる。

ClojureScriptだとデータそのものはImmutableなので、r/atomを渡すのか、r/atomクロージャに持つ関数を渡すのか、などStateをどこに配分してどこからどうやって変更するか、もうちょっと気をつけて考えることが多いと思う。JavaScript通りに書きたいなら全部r/atomにしてしまえばいいのだけど、そうじゃないデザインを自然と志向するようになる。

もしこの○×ゲームを一からReagentで実装するとすれば、多分けっこう違う書き方をすると思う。いつか「Reagent的にはこう書く」というコードも紹介するかも。

Reactチュートリアルの最終形のコード:

gist.github.com

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

最後までやるつもりだったのだが、長くなったので今回はStoring a Historyだけ。

https://facebook.github.io/react/tutorial/tutorial.html#storing-a-history

これまでReactチュートリアルでは、まずSquareを状態を持ったコンポーネントとして実装し、その後状態をBoardコンポーネントに移し、Squareは独自の状態を持たずpropsを通してのみ値を受け取るように変更した。

今回はそれをさらに一歩推し進め、Boardから状態をGameコンポーネントに引き上げる。その上でGameには現在の状態とともに、過去の状態もすべて保持させる。

コードの構造とデータの有無は変わるのだけど、実際のゲーム自体の挙動は何も変わらないので地味である。

状態の定義

Gameコンポーネントのコンストラクタに状態を定義:

class Game extends React.Component {
  constructor() {
    super();
    this.state = {
      history: [{
        squares: Array(9).fill(null)
      }],
      xIsNext: true
    };
  }
  ...
}

今まではBoardコンポーネントのstateの中にsquaresというフィールドがあり、そのフィールドにnullが9つ入ったArrayが入っていた。

今度はGameコンポーネントのstateの中にhistoryというArrayが入っていて、初期設定ではそのArrayに一つオブジェクトが入っている。このオブジェクトが以前Boardに定義されていたstateと同じで、squaresフィールドにnullが9つ入ったArrayが入っている。

ついでにxIsNextもBoardからGameに移行。

ClojureScriptだといつもどおりGameにr/atomを定義する:

(defn Game []
  (let [history (r/atom [{:squares (apply vector (repeat 9 nil))}])
        xIsNext (r/atom true)
        ...]
    ...))

historyはnilが9つ入ったvectorが値で:squareがキーなmapの入ったvectorを格納したr/atom。うーむ。

Render関数

次に、render関数で勝ち負け・次のプレーヤーの表示と、Boardに渡すデータを追加:

class Game extends React.Component {
  ...
  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }
    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

<Board />にsquaresとonClickというpropsが追加されている。そして、以前はBoardで表示していたstatusがGameの<div>{status}</div>に移っている。

ClojureScript:

(defn Game []
  (let [...]
    (fn []
      (let [squares (:squares (last @history))
            status (if-some [winner (calculateWinner squares)]
                     (str "Winner: " winner)
                     (str "Next player: " (if @xIsNext "X" "O")))]
        [:div.game
         [:div.game-board
          [Board squares handle-click]]
         [:div.game-info
          [:div status]
          [:ol]]]))))

render関数の外のletではr/atomを使った状態の定義、中のletではローカル変数定義。

handle-click関数

今回の肝であるhandle-clickのアップデート:

class Game extends React.Component {
  ...
  handleClick(i) {
    const history = this.state.history;
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      history: history.concat([{
        squares: squares
      }]),
      xIsNext: !this.state.xIsNext,
    });
  }
  ...
}

やはりBoardにあったものをあらかた移してきた流れだが、一つ大きな違いがある。

まずthis.state.squaresではなく、this.state.historyから最後(つまり最新)の要素をとり、そのオブジェクトのsquaresフィールドをsquaresとしてコピー。

クリックされた四角の場所に応じてsquaresを更新(以前のsquaresをmutateするのではなく、コピーして変更した新しいsquaresに入れ替える)。そのsquaresを入れたオブジェクトをhistoryに追加している。

ClojureScript:

(defn Game []
  (let [...
        handle-click
        (fn [i]
          (let [squares (:squares (last @history))]
             (if-not (or (calculateWinner squares) 
                         (squares i))
               (let [next-move (if @xIsNext "X" "O")
                     next-squares (assoc squares i next-move)
                     next-step {:squares next-squares}]
                 (do (swap! history #(conj % next-step))
                     (swap! xIsNext not))))))]
    ...))

せっかくなので一歩ずつ解説すると:

  • 現在の状態を(:squares (last @history))でとってきて
  • もし以下二つの条件がどちらもマッチしなければ
    • 勝者がいる
    • クリックされた四角にすでに値が入っている(すでに過去クリックされていた)
  • xIsNextの値によって次の手・プレーヤーを決め
  • squaresのiの位置にその手を入れた新しいsquaresを作成し
  • そのsquaresを入れたmapを作成し
  • そのmapを付け加えるようhistory r/atomの値を更新し
  • xIsNextの値を反転させる

まあそのまんまだ。

Boardコンポーネント

あとはBoardコンポーネントから状態や表示を消すだけ。

class Board extends React.Component {
  renderSquare(i) {
    return <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} />;
  }
  render() {
    return (
      <div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
      ...
      </div>
    );
  }
}

renderSquareは残っているがそれ以外の関数や状態定義は消えた。

Boardにstateがなく、GameからPropsとしてsquaresとonClickが渡されるのでthis.state.squaresだったのがthis.props.squaresなどになっている。

<div {status} />もGameコンポーネントに移行したのでBoardからは消えた。

renderSquareがなければBoardもFunctional Componentにできたのだが。

ClojureScript版:

(defn Board [squares click-fn]
  (let [renderSquare (fn [i] 
                       [Square (get squares i) #(click-fn i)])]
    (fn []
      [:div
       [:div.board-row
        [renderSquare 0]
        [renderSquare 1]
        [renderSquare 2]]
       ...])))

Reagentだとpropsが引数として入ってきて、その引数の名前でlet内のrenderSquareに渡すだけでClosure化するので楽。

追記

おおっと、「楽」と思って油断していたら間違えた。letの中にfn []があると、Boardを作成した時のsquaresとclick-fnで固定されてしまう。解決策としてはsquaresの代わりに何らかのreagent atomを渡すか、renderSquareも毎回生成するようにするか。これだけ簡単かつライトなSPAならrenderのたびにrenderSquareも作成していいと思うが、より複雑・大規模なアプリケーションの場合コスト的にどうなんだろうか。

(defn Board [squares click-fn]
  (let [renderSquare (fn [i] 
                       [Square (get squares i) #(click-fn i)])]
    [:div
     [:div.board-row
      [renderSquare 0]
      [renderSquare 1]
      [renderSquare 2]]
     ...]))

JavaScriptの方はrenderSquareにmutable stateを渡して一回だけ作成する方向なので、atomを使ったほうが合致しているかも。

まとめ

というわけで、冒頭でも言ったがStateの在処を移したのとhistoryという内部データ構造を作っただけで、現在ゲームの挙動は変わらない。次はこのhistoryを表示させるコードを書いていく。

そして今度こそ次回でチュートリアル終了、なはず。

今回のコード(BoardとGameのみ):

gist.github.com

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

公式チュートリアルの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)

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)

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)状態の管理

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に移植してみたい。