Make the conway’s game of life with reagent/re-frame

ライフゲームというものがある。今更説明するほどのものではないと思うけど例えば次の動画は有名かな。

ということで作ってみた。

こういう JS とかで動かせるデモを動かすためにいちいち外部サイトを用意しなくていいのは便利だなと思った。 static なものを置くだけの場所便利である。ちなみに気付いたかもしれないけど、このブログの Home とか Archives とか書いてあるところに Demo って新しく追加してます(今後もこういうのを作っていくかはともかくとして)。

本題

タイトルにあるように Reagent と re-frame を使っています。先日も少し 書いた けど re-frame を使い始めています。 日本語の Reagent/re-frame 情報少ないので、ちょっとだけ具体的にどういうところが良いのかとか書いてみる。

日本だと Reagent より Om の情報の方が多いし使ってる人も多い印象だけど、 Reagent は後発なこともあり、かなりシンプルなので読みやすく書きやすい。パフォーマンスも良いとのことなので、そのへんは Google Closure 様々だと思う。まぁ Om か Reagent かっていう話は結構好みの問題だと思うけど、僕自身 Om はごちゃっとしたイメージあるので書きたくない。

さて、 re-frame 最近、海外の方で熱いぽくて結構熱がある印象。ちょいちょい RSS 読んでると名前は見かける。これは何かというと Reagent にちょっとした縛りを提供してくれるもので、まぁフレームワークの上のフレームワークというか、わりと Reagent 自身は自由にコンポーネント作ったり、データの持ち方も色々と自由に出来るんだけどそれを統一しましょうっていう試みですね。

re-frame is a pattern for writing SPAs in ClojureScript, using Reagent.

とのことなので、まぁパターンですね。実際、 re-frame 自体はとても軽量なものですし、 Reagent を書いていて「あー」って思ったら似たようなものを書きたくなるものかもしれないです(少なくともデキルマンは「いずれ書く」って言ってました)。

Reagent を使う利点としてコンポーネントが書きやすいことにあります。例えば今回書いたコードだとこんなコードがあります。

(defn life [x y live?]
  [:rect {:x x
          :y y
          :width 1
          :height 1
          :stroke "black"
          :stroke-width 0.01
          :rx 0.1
          :fill (if live? :black :white)}])

(defn board []
  (let [current-state (subscribe [::state])
        state-width (count (first @current-state))
        state-height (count @current-state)]
    [:svg {:style {:width 1000
                   :height 1000
                   :border "1px solid black"}
           :view-box (str/join " " [0 0 50 50])}
     (into [:g]
           (for [i (range state-width)
                 j (range state-height)
                 :let [live? (get-in @current-state [i j])]]
             [life i j live?]))]))

(defn life-game-world []
  [:div
   [:span {:style {:display :block}}
    [:h1 {:style {:display :inline}} "Life Game : " (deref (subscribe [::step]))]]
   [board]])

まぁこれが今回書いたコードのコンポーネント全てになります。読めますかね? おそらく Hiccup を使っているのであれば簡単に読めるかと思います。大凡 Hiccup と同じように書けます(というか全く同じ?)。

余程変なことしたいとかじゃなければ、ほとんどの場合はこれだけで足りるんじゃないでしょうか。まぁコンポーネントが書きやすい以上の利点を僕は生憎思いつかないので Om から Reagent に乗り換えた人が書いてくれればいいですが、 Om Next という話もあって Cursor 廃止して re-frame よろしくクエリを導入するみたいな話があるみたいなので、ちょっと Om の今後にも期待してみたいところではあります。

さて、ちらっと出ましたが re-frame の利点についてです。これはあくまでもパターンであり、 reference implementation なので re-frame というとなんとなく違和感があるのですが、まぁ re-frame に沿って書いた場合の利点ということにしておきましょう。

まず全てのデータをひとつの atom に入れてしまってデータベースのように扱うというのはかなり特徴的かなと。色んなところで atom を作って更新するのではなくてデータベースとして、ただひとつの atom を持つことで流れを一本化出来てより明確にイメージしやすくなる気がします。

(def app-db  (reagent/atom {}))

上のようなデータベースを re-frame があらかじめ用意しているので、僕らはこれに対してデータを投入するだけで済みます。

;; ライフゲームのマス目を作るヘルパー関数
(defn new-state [width height]
  (vec (repeatedly width #(vec (repeatedly height (constantly false))))))

;; デフォルトのデータをマップとして用意しておく
(def initial-state
  {:step 0
   :state (new-state 100 100)})

;; イベントハンドラが呼び出されたタイミングでデータベースに対して初期値をマージする
(register-handler
 ::initialize
 (after #(dispatch [::randomize]))
 (fn [db _]
   (merge db initial-state)))

という感じで書けます。

これだけだとまぁそう嬉しさがない気がするんですけど、これにコントローラ層とクエリ層があることで綺麗にデータフローを書けます。

コントローラ層と言っているのはイベントハンドラですね。上記で出ているように register-handler 関数で登録するのですが、呼び出し側はこうなります。

(defn main []
  (when-let [elm (dom/get-element "life-game")]
    (dispatch-sync [::initialize])
    (r/render [life-game-world] elm)
    (js/setInterval #(dispatch [::tick!]) 200)))

dispatch-sync/dispatch 関数で登録していたイベントハンドラを呼び出すことができます。用途としてはイベントを受けて状態を更新するとかですね。

状態を更新してばかりだとどうしようもないので、そこに対応するのがクエリ層です。

(register-sub
 ::state
 (fn [db _]
   (reaction (:state @db))))

これはとてもシンプルなので、もはや書くだけ面倒くさい気がしますけど大事な点は reaction マクロで囲った部分が変更されたときのみに subscriber に通知されるということです。 例えばこれが id みたいなものを受け取るようなクエリだった場合、その id を使ってデータベースから取得した結果が変わったときに、その id を渡していた subscriber にのみ通知がいくので、その部分だけに変更が適応されるということ。まぁ書いてもなかなか理解しにくいと思うのでこれは実際に書いてみて便利さを感じて欲しいですね。

read-only な Cursor とかに近いと思います。 Om は Cursor からクエリに行きそうな感じですけど。ちなみに README 自体には read-only Cursor よりも便利だよ!って書いてあります。

だいたいこんな感じで Reagent/re-frame を使ってライフゲームを書いたんですが、わりと気持よく書けました。

ライフゲームについて

流石に 100x100 でもナイーブに書くと動きが重いですね。 hashlife とか実装があるので、そのへんのやり方で実装すればいいんでしょうけど、今回はもともと You should be using Figwheel/Reagent. Here’s why: という記事を読んでライフゲーム書けるんじゃね?って思ったのでやってみた感じです。

今回書いてみて実際に自分のマインドとかなり一致した書き方ができたので良かったですね。自分の中のイメージが直感的に書きくだせたので僕には Clojure な考え方があってるんでしょうね。