ClojureScript/re-frame開発における思考フロー

Clojurianの[lagénorhynque](https://twitter.com/lagenorhynque) (a.k.a. カマイルカ)です。

先日、[Shibuya.lisp lispmeetup #62](https://lisp.connpass.com/event/81706/)で「[re-frame *à la spec*](https://www.slideshare.net/KentOhashi/reframe-la-spec)」と題してre-frameというClojureScriptフレームワークの概要とclojure.specとの統合方法の一例について発表しました。

本記事では、その発表内容に関連してClojureScriptとre-frameでのSPA開発の流れについてご紹介します。

@ababup1192 さんの[Elm](http://elm-lang.org/)についての素晴らしい記事 [Elm開発における思考フロー](https://qiita.com/ababup1192/items/629585f8b2ae953a9b47) からサンプルコードと本文の構成を参考にさせていただきました。

こちらの記事と比較してみると、両言語/アーキテクチャの共通点や差異が見られて面白いかもしれません。

※ 本記事はClojure/ClojureScript, Reagent, clojure.specに対する基本的な理解を一応の前提としているため、馴染みのないものがある方はFurther Readingの各種ドキュメントなどを適宜ご確認ください。

# re-frameとは

[re-frame](https://github.com/day8/re-frame)は[ClojureScript](https://clojurescript.org/)のReactラッパーのひとつ[Reagent](https://reagent-project.github.io/)を基礎とした状態管理フレームワークです。

位置付けとしては

- [Elm Architecture](https://guide.elm-lang.org/architecture/)

- [Redux](https://redux.js.org/) (React)

- [ngrx](https://github.com/ngrx/platform) (Angular)

- [Vuex](https://github.com/vuejs/vuex) (Vue.js)

などと同種のものです。

詳細はGitHubの[docs](https://github.com/day8/re-frame/tree/master/docs)にドキュメントがありますが、

1. event dispatch

1. event handling

1. effect handling

1. query

1. view

1. DOM

という単方向のデータフローを利用してアプリケーションを構築します。

# ClojureScript & re-frameによるSPA開発

参考元のElmの記事に倣って、じゃんけんゲーム(rock-paper-scissors)アプリを開発してみます。

※ ビルドやテストの設定、実行方法などは本記事では省略しているため、詳細はこちらのサンプルコードリポジトリをご確認ください: [lagenorhynque/rock-paper-scissors](https://github.com/lagenorhynque/rock-paper-scissors/tree/simple-view)

## 0. プロジェクトを構成する

事前準備として、re-frameベースのClojureScriptプロジェクトを生成します。

re-frameにはLeiningen向けのテンプレート[re-frame-template](https://github.com/day8/re-frame-template)が提供されているので、以下のようなコマンドでプロジェクトが生成できます。

```sh
$ lein new re-frame rock-paper-scissors +cider +test
```

オプションの `+cider` でEmacsのCIDERサポート、 `+test` でClojureScriptのテスト関連サポート、 `+10x` でre-frameのデバッグ支援ツール[re-frame-10x](https://github.com/day8/re-frame-10x)を追加してみました。

以下のようなディレクトリ構成のプロジェクトが生成されます。

```sh
$ tree rock-paper-scissors
rock-paper-scissors
├── README.md
├── project.clj
├── resources
│   └── public
│       └── index.html
├── src
│   ├── clj
│   │   └── rock_paper_scissors
│   │       └── core.clj
│   └── cljs
│       └── rock_paper_scissors
│           ├── config.cljs
│           ├── core.cljs
│           ├── db.cljs
│           ├── events.cljs
│           ├── subs.cljs
│           └── views.cljs
└── test
    └── cljs
        └── rock_paper_scissors
            ├── core_test.cljs
            └── runner.cljs
```

今回のプロジェクトでは、このあと利用するclojure.specでspecの定義を通常のソースコードとは完全に別のディレクトリの別の名前空間(`specs` ディレクトリ配下の `specs.cljs`)に配置する方針を採るため([こちら](https://www.slideshare.net/KentOhashi/reframe-la-spec/44)を参照)、最終的には以下のようなディレクトリ構成になります。

```sh
rock-paper-scissors
├── README.md
├── project.clj
├── resources
│   └── public
│       ├── css
│       │   └── style.css
│       └── index.html
├── specs
│   └── cljs
│       └── rock_paper_scissors
│           ├── db
│           │   └── specs.cljs
│           └── rps
│               └── specs.cljs
├── src
│   ├── clj
│   │   └── rock_paper_scissors
│   │       └── core.clj
│   └── cljs
│       └── rock_paper_scissors
│           ├── cofx.cljs
│           ├── config.cljs
│           ├── core.cljs
│           ├── db.cljs
│           ├── events.cljs
│           ├── rps.cljs
│           ├── subs.cljs
│           └── views.cljs
└── test
    └── cljs
        └── rock_paper_scissors
            ├── events_test.cljs
            ├── rps_test.cljs
            └── runner.cljs
```

## 1. ユニットテストを使いビジネスロジックを考える

最初に今回のアプリのコアとなるドメイン、じゃんけんゲームのビジネスロジックを実装します。

いきなり必要な関数の実装を始めることもできますが、ここでは

1. ユニットテストを書く

1. clojure.specで扱うデータと関数の仕様を記述する

1. 関数を実装する

という手順でテスト駆動開発(TDD)的に進めてみることにします。

Clojure/ClojureScriptはElmとは異なり非純粋で動的な関数型言語ですが、関数の純粋性を重視しており、原則としてイミュータブルなデータを扱うため値の等価性判定も容易です。

エディタと統合された強力なREPL環境(Emacsの[CIDER](https://github.com/clojure-emacs/cider)、IntelliJの[Cursive](https://cursive-ide.com/)など)を利用した、Lispらしい[REPL駆動開発](https://qiita.com/lagenorhynque/items/d68934546fa7283bad9d)(REPL-driven development)が大きな強みのひとつですが、TDDとの併用も効果的です。

上記1〜3の順序は便宜的なもので必ずしも縛られる必要はなく、REPLで素早くフィードバックを得ながらテスト、spec、実装をインクリメンタルに書き進めていくのが良さそうです。

2番目の[clojure.spec](https://clojure.org/guides/spec)の利用は必須ではありませんが、Clojure/ClojureScriptにおいて静的言語の型チェックに代わる仕組みとして変更に強いプログラムにするためにも、特に重要なドメインロジックについてはspecを書いておくことをお勧めします。

### ユニットテストを書く

名前空間 `rock-paper-scissors.rps` にじゃんけんゲームのビジネスロジックを実装する想定で、以下のように `fight` 関数の振る舞いをテストコードで表現してみます。

[test/cljs/rock_paper_scissors/rps_test.cljs](https://github.com/lagenorhynque/rock-paper-scissors/blob/c6930da605452fd939f40e1f2c1c803afc047b6f/test/cljs/rock_paper_scissors/rps_test.cljs): じゃんけんロジックのexample-based testを作成

```clj
(ns rock-paper-scissors.rps-test
  (:require [cljs.test :as t :include-macros true]
            [rock-paper-scissors.rps :as sut]))

(t/deftest test-fight
  (t/testing "rock-paper-scissors"
    (t/is (= ::sut/win (sut/fight ::sut/rock ::sut/scissors)))
    (t/is (= ::sut/lose (sut/fight ::sut/scissors ::sut/rock)))
    (t/is (= ::sut/draw (sut/fight ::sut/paper ::sut/paper)))))
```

テストケースを書いたら、早速テストを実行してパスしないことを確認します(`rock-paper-scissors.rps` という名前空間が未定義なので当然エラーになります)。

```sh
$ lein doo phantom test
...
clojure.lang.ExceptionInfo: failed compiling file:test/cljs/rock_paper_scissors/rps_test.cljs {:file #object[java.io.File 0x6619692e "test/cljs/rock_paper_scissors/rps_test.cljs"]}
	at clojure.core$ex_info.invokeStatic(core.clj:4739)
	at clojure.core$ex_info.invoke(core.clj:4739)
	at cljs.compiler$compile_file$fn__4585.invoke(compiler.cljc:1552)
	at cljs.compiler$compile_file.invokeStatic(compiler.cljc:1513)
	at cljs.compiler$compile_file.invoke(compiler.cljc:1489)
	at cljs.closure$compile_file.invokeStatic(closure.clj:540)
	at cljs.closure$compile_file.invoke(closure.clj:531)
	at cljs.closure$eval6840$fn__6841.invoke(closure.clj:609)
	at cljs.closure$eval6776$fn__6777$G__6765__6784.invoke(closure.clj:493)
	at cljs.closure$compile_sources$iter__6964__6968$fn__6969.invoke(closure.clj:954)
...
Caused by: clojure.lang.ExceptionInfo: No such namespace: rock-paper-scissors.rps, could not locate rock_paper_scissors/rps.cljs, rock_paper_scissors/rps.cljc, or JavaScript source providing "rock-paper-scissors.rps" in file test/cljs/rock_paper_scissors/rps_test.cljs {:tag :cljs/analysis-error}
...
```

### clojure.specで扱うデータと関数の仕様を記述する

じゃんけんゲームのビジネスロジックに関わるデータと関数はどのようなものか、clojure.specを利用して記述します。

| spec                          | description                                                         |
| ---                           | ---                                                                 |
| 手 `::hand`                   | キーワード `::rps/rock`, `::rps/paper`, `::rps/scissors` のいずれか |
| 手に対応する数値 `::hand-num` | 0以上3未満の整数                                                    |
| 結果(勝敗) `::result`         | キーワード `::rps/win`, `::rps/lose`, `::rps/draw` のいずれか       |
| 関数 `rps/<-hand`             | `::hand` を受け取って `::hand-num` を返す                           |
| 関数 `rps/->hand`             | `::hand-num` を受け取って `::hand` を返す                           |
| 関数 `rps/fight`              | あなたと対戦相手の `::hand` をそれぞれ受け取って `::result` を返す  |

[specs/cljs/rock_paper_scissors/rps/specs.cljs](https://github.com/lagenorhynque/rock-paper-scissors/blob/simple-view/specs/cljs/rock_paper_scissors/rps/specs.cljs): じゃんけんロジックのspecを定義

```clj
(ns rock-paper-scissors.rps.specs
  (:require [cljs.spec.alpha :as s :include-macros true]
            [rock-paper-scissors.rps :as rps]))

(s/def ::hand #{::rps/rock ::rps/paper ::rps/scissors})

(s/def ::hand-num (s/int-in 0 3))

(s/def ::result #{::rps/win ::rps/lose ::rps/draw})

(s/fdef rps/<-hand
  :args (s/cat :hand ::hand)
  :ret ::hand-num)

(s/fdef rps/->hand
  :args (s/cat :num ::hand-num)
  :ret ::hand)

(s/fdef rps/fight
  :args (s/cat :you ::hand
               :enemy ::hand)
  :ret ::result)
```

関数のspecを活用してユニットテストに簡単なプロパティベースの(property-based)テストを追加しておくこともできます。

ここでは、引数のspecを満たす値をランダムに生成して関数に適用し、戻り値がそのspecを満たすことを確認する操作を1000回試行するテストを書いてみました(今回の場合は1000回も試す意味はない😅)。

[test/cljs/rock_paper_scissors/rps_test.cljs](https://github.com/lagenorhynque/rock-paper-scissors/blob/simple-view/test/cljs/rock_paper_scissors/rps_test.cljs): じゃんけんロジックのproperty-based testを追加

```clj
(ns rock-paper-scissors.rps-test
  (:require [cljs.spec.alpha :as s]
            [cljs.spec.test.alpha :as stest :include-macros true]
            [cljs.test :as t :include-macros true]
            [clojure.test.check.clojure-test :as tc :include-macros true]
            [clojure.test.check.properties :as prop :include-macros true]
            [rock-paper-scissors.rps :as sut]
            [rock-paper-scissors.rps.specs]))

(t/use-fixtures
  :once {:before #(stest/instrument)})

(t/deftest test-fight
  (t/testing "rock-paper-scissors"
    (t/is (= ::sut/win (sut/fight ::sut/rock ::sut/scissors)))
    (t/is (= ::sut/lose (sut/fight ::sut/scissors ::sut/rock)))
    (t/is (= ::sut/draw (sut/fight ::sut/paper ::sut/paper)))))

(tc/defspec prop-test-<-hand
  1000
  (let [fspec (s/get-spec #'sut/<-hand)]
    (prop/for-all [[hand] (-> fspec :args s/gen)]
      (s/valid? (:ret fspec)
                (sut/<-hand hand)))))

(tc/defspec prop-test-->hand
  1000
  (let [fspec (s/get-spec #'sut/->hand)]
    (prop/for-all [[num] (-> fspec :args s/gen)]
      (s/valid? (:ret fspec)
                (sut/->hand num)))))

(tc/defspec prop-test-fight
  1000
  (let [fspec (s/get-spec #'sut/fight)]
    (prop/for-all [[you enemy] (-> fspec :args s/gen)]
      (s/valid? (:ret fspec)
                (sut/fight you enemy)))))
```

### 関数を実装する

あとは、specを満たしてユニットテストをパスするように関数を実装するだけです。

[src/cljs/rock_paper_scissors/rps.cljs](https://github.com/lagenorhynque/rock-paper-scissors/blob/simple-view/src/cljs/rock_paper_scissors/rps.cljs): じゃんけんロジックを実装

```clj
(ns rock-paper-scissors.rps)

(defn <-hand [hand]
  (case hand
    ::rock 0
    ::scissors 1
    ::paper 2))

(defn ->hand [num]
  (case num
    0 ::rock
    1 ::scissors
    2 ::paper))

(defn fight [you enemy]
  (letfn [(result [n]
            (case n
              0 ::draw
              1 ::lose
              2 ::win))]
    (-> (- (<-hand you) (<-hand enemy))
        (+ 3)
        (mod 3)
        result)))
```

期待通りすべてのテストケースをパスすることが確認できます。

```sh
$ lein doo phantom test
...

;; ======================================================================
;; Testing with Phantom:


Testing rock-paper-scissors.rps-test

Ran 1 tests containing 3 assertions.
0 failures, 0 errors.
```

## 2. 状態遷移について考える

ビジネスロジックが仕上がったので、ここからはre-frameを導入して画面を開発していきます。

参考元のElmでの例と同様に、画面状態は"Start", "Now Playing", "Over"の3種類があり、画面遷移をこれらの状態遷移として表現することにします。

## 3. 状態についてコードを書く

### **db**

re-fameではアプリケーションの状態を **db** というマップを保持する単一の[Reagent](https://reagent-project.github.io/) atomで集中管理します。

そこで、今回のじゃんけんゲームアプリにおける **db** マップの仕様をspecで記述してみます(**db** にspecを付けておくと、開発時やテスト時にアプリケーションが不正な状態になればspecのエラーとして直ちに検出することも可能になります)。

`::db` は以下のエントリを持つマップとします。

| key      | value                                                             |
| ---      | ---                                                               |
| `:you`   | じゃんけんゲームのあなたの手 `::rps.specs/hand`                   |
| `:enemy` | じゃんけんゲームの対戦相手の手 `::rps.specs/hand`                 |
| `:scene` | 画面状態 `::db/start`, `::db/now-playing`, `::db/over` のいずれか |

[specs/cljs/rock_paper_scissors/db/specs.cljs](https://github.com/lagenorhynque/rock-paper-scissors/blob/simple-view/specs/cljs/rock_paper_scissors/db/specs.cljs): re-frameのdbのspecを定義

```clj
(ns rock-paper-scissors.db.specs
  (:require [cljs.spec.alpha :as s :include-macros true]
            [rock-paper-scissors.db :as db]
            [rock-paper-scissors.rps.specs :as rps.specs]))

(s/def ::you ::rps.specs/hand)

(s/def ::enemy ::rps.specs/hand)

(s/def ::scene #{::db/start ::db/now-playing ::db/over})

(s/def ::db (s/keys :req-un [::you ::enemy ::scene]))
```

そして、この **db** の仕様を満たすようにアプリケーション起動時の初期値を設定します。

ここでは、プレイヤー(あなた)と対戦相手の手はどちらもグー、画面状態は"Start"としました。

[src/cljs/rock_paper_scissors/db.cljs](https://github.com/lagenorhynque/rock-paper-scissors/blob/simple-view/src/cljs/rock_paper_scissors/db.cljs): re-frameのdbの初期値を定義

```clj
(ns rock-paper-scissors.db
  (:require [rock-paper-scissors.rps :as rps]))

(def default-db
  {:you ::rps/rock
   :enemy ::rps/rock
   :scene ::start})
```

## 4. 状態を遷移するための入力について考える

### **event**

re-frameではアプリケーション状態 **db** の変更は **event** によって行います。

**event** は `[<event-id> <arg>*]` という形式のベクターとして表現します。

Elmでの実装に倣って、画面状態"Now Playing"への遷移は `[::next-game]` 、"Over"への遷移 `[::select-your-hand h]` という **event** によって実現することにします。

## 5. 状態遷移についてコードを書く

### **event handler**

それでは実際にre-frameの **event** を扱う関数(= **event handler**)を実装してみます。

[`re-frame.core/reg-event-db`](https://github.com/day8/re-frame/blob/v0.10.5/src/re_frame/core.cljc#L71-L84) 関数を利用すると、 **db** と **event** を引数に取って新しい **db** を返す関数(Haskell風に書けば `(db, event) -> db`)に名前を付けて **event handler** として登録することができます。

```clj
(re-frame.core/reg-event-db
  <event-id>
  (fn [<db> <event>]
    <db>))
```

このように登録された **event handler** 自体は純粋な関数ですが、最終的にはre-frame組み込みの **effect handler** [`:db`](https://github.com/day8/re-frame/blob/v0.10.5/src/re_frame/fx.cljc#L162-L166) を介して **db** 更新という副作用(**effect**)が発生することになります。

ここでは、以下の3種類の **event handler** を実装します。

| event-id             | description                                                                        |
| ---                  | ---                                                                                |
| `::initialize-db`    | 現在の状態にかかわらず **db** に初期値 `db/default-db` を設定する                  |
| `::next-game`        | 画面状態を"Now Playing"(`::db/now-playing`)に変更する                              |
| `::select-your-hand` | あなたの手 `h` を受け取って設定するとともに画面状態を"Over"(`::db/over`)に変更する |

[src/cljs/rock_paper_scissors/events.cljs](https://github.com/lagenorhynque/rock-paper-scissors/blob/e6dea82873cec7a62484d06a3768179b0665ab93/src/cljs/rock_paper_scissors/events.cljs): re-frameのevent handlerを実装

```clj
(ns rock-paper-scissors.events
  (:require [re-frame.core :as re-frame]
            [rock-paper-scissors.db :as db]))

(re-frame/reg-event-db
  ::initialize-db
  (fn  [_ _]
    db/default-db))

(re-frame/reg-event-db
  ::next-game
  (fn [db _]
    (assoc db :scene ::db/now-playing)))

(re-frame/reg-event-db
  ::select-your-hand
  (fn [db [_ h]]
    (assoc db
           :you h
           :scene ::db/over)))
```

## 6. 状態をもとに画面の描画をするコードを書く

最後に、 **db** を **query** で参照して **view** を描画し、適宜 **event** を発生させることで動作する画面に仕上げます。

### **query** と **subscription**

re-frameでは **db** の **query** のために **subscription** を利用します。

**query** は `[<query-id> <arg>*]` という形式のベクターで表現されます。

[`re-frame.core/reg-sub`](https://github.com/day8/re-frame/blob/v0.10.5/src/re_frame/core.cljc#L45) 関数を利用すると、 **db** と **query** を引数に取って任意の値を返す関数(Haskell風に書けば `(db, query) -> value`)に名前をつけて **subscription** として登録することができます。

```clj
(re-frame.core/reg-sub
  <query-id>
  (fn [<db> <query>]
    <value>))
```

このように登録された **subscription** の関数は **db** が更新されるたびに呼び出され、常に最新の状態を反映した値を取り出すことができます。

ここでは、以下の2種類の **subscription** を実装します。

| query-id      | description                                                      |
| ---           | ---                                                              |
| `::scene`     | 画面状態 `:scene` を取り出す                                     |
| `::you-enemy` | あなたの手 `:you` と対戦相手の手 `:enemy` をマップとして取り出す |

[src/cljs/rock_paper_scissors/subs.cljs](https://github.com/lagenorhynque/rock-paper-scissors/blob/017a3390f105c43f980797086a82d99a1123ebf4/src/cljs/rock_paper_scissors/subs.cljs): re-frameのsubscriptionを実装

```clj
(ns rock-paper-scissors.subs
  (:require [re-frame.core :as re-frame]))

(re-frame/reg-sub
  ::scene
  (fn [db _]
    (:scene db)))

(re-frame/reg-sub
  ::you-enemy
  (fn [db _]
    (select-keys db [:you :enemy])))
```

### **view**

あとは、ここまでで用意したじゃんけんゲームのロジック、状態を変更する **event** 、状態を参照する **subscription** を利用して画面を描画する **view** を実装するだけです。

**event** は [`(re-frame.core/dispatch <event>)`](https://github.com/day8/re-frame/blob/v0.10.5/src/re_frame/core.cljc#L40) でディスパッチし、 **subscription** は [`@(re-frame.core/subscribe <query>)`](https://github.com/day8/re-frame/blob/v0.10.5/src/re_frame/core.cljc#L46) で購読することができます(見た目に冗長と思われる場合には、[こちら](https://github.com/day8/re-frame/blob/v0.10.5/docs/SubscriptionsCleanup.md#lambdaisland-naming--lin)のように `>evt`, `<sub` といった別名を付けても良いかもしれません)。

Reagentの[Hiccup](https://github.com/weavejester/hiccup)風のDSLを利用して、例えば以下のように **view** のコンポーネントを実装することができるでしょう。

[src/cljs/rock_paper_scissors/views.cljs](https://github.com/lagenorhynque/rock-paper-scissors/blob/017a3390f105c43f980797086a82d99a1123ebf4/src/cljs/rock_paper_scissors/views.cljs): re-frameのviewを実装

```clj
(ns rock-paper-scissors.views
  (:require [re-frame.core :as re-frame]
            [rock-paper-scissors.db :as db]
            [rock-paper-scissors.events :as events]
            [rock-paper-scissors.rps :as rps]
            [rock-paper-scissors.subs :as subs]))

(defn hands []
  [:div (map (fn [h]
               ^{:key h}
               [:input {:type "button"
                        :on-click #(re-frame/dispatch [::events/select-your-hand h])
                        :value h}])
             [::rps/rock ::rps/paper ::rps/scissors])])

(defn result [you enemy]
  (let [r (rps/fight you enemy)]
    (case r
      ::rps/win [:h1 {:style {:color "red"}}
                 r]
      ::rps/lose [:h1 {:style {:color "blue"}}
                  r]
      ::rps/draw [:h1 {:style {:color "gray"}}
                  r])))

(defn main-panel []
  (let [scene @(re-frame/subscribe [::subs/scene])]
    (case scene
      ::db/start [:input {:type "button"
                          :on-click #(re-frame/dispatch [::events/next-game])
                          :value "Game Start"}]
      ::db/now-playing [hands]
      ::db/over (let [{:keys [you enemy]} @(re-frame/subscribe [::subs/you-enemy])]
                  [:div
                   [:h1 (str (name you) "(YOU) VS " (name enemy) "(ENEMY)")]
                   [result you enemy]
                   [:input {:type "button"
                            :on-click #(re-frame/dispatch [::events/next-game])
                            :value "Next Game"}]]))))
```

ブラウザで試してみると、以下の3画面の動作が確認できます。

- "Start": 「Game Start」ボタン押下で"Now Playing"へ

![Start](https://gist.githubusercontent.com/lagenorhynque/4a5c6557b62ba09ba513cc93b40c95e3/raw/796826aa5664aa04ee0d16dfc92fc4356bc83840/rock-paper-scissors_01.png)

- "Now Playing": 「rock」「paper」「scissors」ボタン押下で"Over"へ

![Now Playing](https://gist.githubusercontent.com/lagenorhynque/4a5c6557b62ba09ba513cc93b40c95e3/raw/796826aa5664aa04ee0d16dfc92fc4356bc83840/rock-paper-scissors_02.png)

- "Over": 「Next Game」ボタン押下で"Now Playing"へ

![Over](https://gist.githubusercontent.com/lagenorhynque/4a5c6557b62ba09ba513cc93b40c95e3/raw/796826aa5664aa04ee0d16dfc92fc4356bc83840/rock-paper-scissors_05.png)

## 7. 副作用について考える

ここまでで画面の機能は一通り完成しましたが、じゃんけんゲームで対戦相手の手が **db** の初期値で設定したグー固定になっているという問題があります。

簡単な対応として、 **event handler** `::select-you-hand` でプレイヤー(あなた)の手を設定するとともに乱数生成された値で対戦相手の手を設定してしまうこともできますが、乱数生成という典型的な副作用によって **event handler** 関数の純粋性が失われてしまいます。

### **coeffect** と **effect**

シンプルなデータと純粋関数を可能な限り利用し、副作用を明確に分離することを重視するre-frameでは、このような場合に **coeffect** というものを活用することができます。

**event handler** を登録するために利用した `re-frame.core/reg-event-db` 関数は

```clj
(re-frame.core/reg-event-db
  <event-id>
  (fn [<db> <event>]
    <db>))
```

という形式でしたが、実はこれは **db** 更新に特化した特殊な関数で、より一般的には [`re-frame.core/reg-event-fx`](https://github.com/day8/re-frame/blob/v0.10.5/src/re_frame/core.cljc#L87-L101) 関数で

```clj
(re-frame.core/reg-event-fx
  <event-id>
  [<interceptor>*]
  (fn [<coeffect-map> <event>]
    <effect-map>))
```

という形式で **coeffect** マップと **event** を引数に取って **effect** マップを返す関数(Haskell風に書けば `(coeffect-map, event) -> effect-map`)に名前をつけて **event handler** として登録することができます。

例えば **event handler** `::select-you-hand` は `re-frame.core/reg-event-fx` を利用すると以下のように登録することができます。

```clj
(re-frame/reg-event-fx
  ::select-your-hand
  []
  (fn [{:keys [db]} [_ h]]
    {:db (assoc db
                :you h
                :enemy enemy-hand
                :scene ::db/over)}))
```

このとき、 **event handler** 関数の第1引数が **coeffect** マップ `{:db ,,,}` 、返り値が **effect** マップ `{:db ,,,}` です。

- **coeffect**
    - **event handler** の入力
    - [`re-frame.core/reg-cofx`](https://github.com/day8/re-frame/blob/v0.10.5/src/re_frame/core.cljc#L64) で登録した **coeffect handler** によって外部からデータ取得する
        - 組み込みの例: [`:db`](https://github.com/day8/re-frame/blob/v0.10.5/src/re_frame/cofx.cljc#L104-L108) (現在の **db** の値を取得する)

- **effect**
    - **event handler** の出力
    - [`re-frame.core/reg-fx`](https://github.com/day8/re-frame/blob/v0.10.5/src/re_frame/core.cljc#L60) で登録した **effect handler** によって副作用が発生する
        - 組み込みの例: [`:db`](https://github.com/day8/re-frame/blob/v0.10.5/src/re_frame/fx.cljc#L162-L166) (指定された値で **db** を更新する)

**coeffect** は **event handler** の入力として外部からデータを取得するために、 **effect** は **event handler** の出力として副作用を発生させるために利用できるので、今回の場合は **coeffect** で乱数生成して対戦相手の手を取得することにしましょう。

`re-frame.core/reg-cofx` 関数を利用すると、 **coeffect** マップ(と任意の引数)を取って新しい **coeffect** マップを返す関数(Haskell風に書けば `(coeffect-map, arg) -> coeffect-map`)に名前をつけて **coeffect handler** として登録することができます。

```clj
(re-frame.core/reg-cofx
  <coeffect-id>
  (fn [<coeffect-map> <arg>]
    <coeffect-map>))
```

ここでは、 `(rand-int 3)` で0以上3未満の整数を乱数で取得してじゃんけんの手に変換し、 `:enemy-hand` というキーで **coeffect** マップに加える **coeffect handler** `::select-enemy-hand` として実装してみました。

[src/cljs/rock_paper_scissors/cofx.cljs](https://github.com/lagenorhynque/rock-paper-scissors/blob/simple-view/src/cljs/rock_paper_scissors/cofx.cljs): re-frameのcoeffect handlerを実装

```clj
(ns rock-paper-scissors.cofx
  (:require [re-frame.core :as re-frame]
            [rock-paper-scissors.rps :as rps]))

(re-frame/reg-cofx
  ::select-enemy-hand
  (fn [cofx _]
    (assoc cofx
           :enemy-hand (rps/->hand (rand-int 3)))))
```

**event handler** `::select-your-hand` を上述のように `re-frame.core/reg-event-fx` で登録するように書き換え、 [`re-frame.core/inject-cofx`](https://github.com/day8/re-frame/blob/v0.10.5/src/re_frame/core.cljc#L65)で **coeffect handler** `::select-enemy-hand` を **interceptor** として組み込むと、乱数生成された対戦相手の手 `:enemy-hand` が利用可能になります。

**coeffect** で乱数として与えられた対戦相手の手を **db** に設定することで、**event handler** 関数の純粋性を保ったまま、実行するたびに対戦相手の手が変わるという挙動を実現することができます。

[src/cljs/rock_paper_scissors/events.cljs](https://github.com/lagenorhynque/rock-paper-scissors/blob/7ca74b2fefddfeaf9c5119fd528ce8a7b5255367/src/cljs/rock_paper_scissors/events.cljs): re-frameのeventにcoeffectを組み込む

```diff
 (ns rock-paper-scissors.events
   (:require [re-frame.core :as re-frame]
+            [rock-paper-scissors.cofx :as cofx]
             [rock-paper-scissors.db :as db]))

 (re-frame/reg-event-db
   ::initialize-db
   (fn  [_ _]
     db/default-db))

 (re-frame/reg-event-db
   ::next-game
   (fn [db _]
     (assoc db :scene ::db/now-playing)))

-(re-frame/reg-event-db
+(re-frame/reg-event-fx
   ::select-your-hand
-  (fn [db [_ h]]
-    (assoc db
-           :you h
-           :scene ::db/over)))
+  [(re-frame/inject-cofx ::cofx/select-enemy-hand)]
+  (fn [{:keys [db enemy-hand]} [_ h]]
+    {:db (assoc db
+                :you h
+                :enemy enemy-hand
+                :scene ::db/over)}))
```

## (+α) 8. リファクタリングする

以上で参考元記事のElm版と同等の機能が実現できましたが、せっかくなのでさらに進んでよりre-frameらしい実装にリファクタリングしてみます。

re-frameでは **view** がロジックを持つのは最小限にし、 **db** データの加工などは **subscription** に集約するのが良いとされています(cf. [Subscriptions Cleanup](https://github.com/day8/re-frame/blob/v0.10.5/docs/SubscriptionsCleanup.md))。

そこで、 **view** のコンポーネントにあった、プレイヤー(あなた)と対戦相手の手の表示文字列の生成、対戦結果の計算、対戦結果による文字色の切り替えを **subscription** として抽出してみました。

関心の分離によって **view** の見通しが良くなるのはもちろん、 **subscription** を階層化したことで再計算を必要最小限に留めることが可能になり効率化も見込めます。

[src/cljs/rock_paper_scissors/subs.cljs](https://github.com/lagenorhynque/rock-paper-scissors/blob/simple-view/src/cljs/rock_paper_scissors/subs.cljs): re-frameのviewのロジックをsubscriptionに抽出

```diff
 (ns rock-paper-scissors.subs
-  (:require [re-frame.core :as re-frame]))
+  (:require [re-frame.core :as re-frame]
+            [rock-paper-scissors.rps :as rps]))

 (re-frame/reg-sub
   ::scene
   (fn [db _]
     (:scene db)))

 (re-frame/reg-sub
   ::you-enemy
   (fn [db _]
     (select-keys db [:you :enemy])))
+
+(re-frame/reg-sub
+  ::you-enemy-hands
+  :<- [::you-enemy]
+  (fn [{:keys [you enemy]} _]
+    (str (name you) "(YOU) VS " (name enemy) "(ENEMY)")))
+
+(re-frame/reg-sub
+  ::fight-result
+  :<- [::you-enemy]
+  (fn [{:keys [you enemy]} _]
+    (rps/fight you enemy)))
+
+(re-frame/reg-sub
+  ::result-color
+  :<- [::fight-result]
+  (fn [r _]
+    (case r
+      ::rps/win "red"
+      ::rps/lose "blue"
+      ::rps/draw "gray")))
```

[src/cljs/rock_paper_scissors/views.cljs](https://github.com/lagenorhynque/rock-paper-scissors/blob/simple-view/src/cljs/rock_paper_scissors/views.cljs): re-frameのviewのコンポーネントを整理

```diff
 (ns rock-paper-scissors.views
   (:require [re-frame.core :as re-frame]
             [rock-paper-scissors.db :as db]
             [rock-paper-scissors.events :as events]
             [rock-paper-scissors.rps :as rps]
             [rock-paper-scissors.subs :as subs]))

+(defn start-button [label]
+  [:input {:type "button"
+           :on-click #(re-frame/dispatch [::events/next-game])
+           :value label}])
+
 (defn hands []
   [:div (map (fn [h]
                ^{:key h}
                [:input {:type "button"
                         :on-click #(re-frame/dispatch [::events/select-your-hand h])
                         :value h}])
              [::rps/rock ::rps/paper ::rps/scissors])])

-(defn result [you enemy]
-  (let [r (rps/fight you enemy)]
-    (case r
-      ::rps/win [:h1 {:style {:color "red"}}
-                 r]
-      ::rps/lose [:h1 {:style {:color "blue"}}
-                  r]
-      ::rps/draw [:h1 {:style {:color "gray"}}
-                  r])))
+(defn result []
+  (let [r @(re-frame/subscribe [::subs/fight-result])]
+    [:h1 {:style {:color @(re-frame/subscribe [::subs/result-color])}}
+     r]))

 (defn main-panel []
-  (let [scene @(re-frame/subscribe [::subs/scene])]
-    (case scene
-      ::db/start [:input {:type "button"
-                          :on-click #(re-frame/dispatch [::events/next-game])
-                          :value "Game Start"}]
-      ::db/now-playing [hands]
-      ::db/over (let [{:keys [you enemy]} @(re-frame/subscribe [::subs/you-enemy])]
-                  [:div
-                   [:h1 (str (name you) "(YOU) VS " (name enemy) "(ENEMY)")]
-                   [result you enemy]
-                   [:input {:type "button"
-                            :on-click #(re-frame/dispatch [::events/next-game])
-                            :value "Next Game"}]]))))
+  (case @(re-frame/subscribe [::subs/scene])
+    ::db/start [start-button "Game Start"]
+    ::db/now-playing [hands]
+    ::db/over [:div
+               [:h1 @(re-frame/subscribe [::subs/you-enemy-hands])]
+               [result]
+               [start-button "Next Game"]]))
```

## (+α) 9. re-frameのテストを書く

re-frameのユニットテストも書いてみましょう。

テストの方針もいくつか考えられますが(cf. [Testing](https://github.com/day8/re-frame/blob/v0.10.5/docs/Testing.md))、ここでは **event** と **subscription** に対してテストすることにします([こちら](https://www.slideshare.net/KentOhashi/reframe-la-spec/50)を参照)。

**event handler** などほとんどの関数が純粋関数なので、[re-frame-test](https://github.com/day8/re-frame-test)のユーティリティを利用して簡単にテストを書くことができます。

clojure.specで **db** のspecを定義してあるので、 `re-frame.core/reg-fx` で組み込みの **effect handler** [`:db`](https://github.com/day8/re-frame/blob/v0.10.5/src/re_frame/fx.cljc#L162-L166) を再定義し、テスト実行時に **db** がspecを満たさない不正な状態になった場合にはエラーを発生させるようにしてみました。

[test/cljs/rock_paper_scissors/events_test.cljs](https://github.com/lagenorhynque/rock-paper-scissors/blob/simple-view/test/cljs/rock_paper_scissors/events_test.cljs): re-frameのeventに対するテストを作成

```clj
(ns rock-paper-scissors.events-test
  (:require [cljs.spec.alpha :as s]
            [cljs.spec.test.alpha :as stest :include-macros true]
            [cljs.test :as t :include-macros true]
            [day8.re-frame.test :as re-frame.test :include-macros true]
            [re-frame.core :as re-frame]
            [re-frame.db :refer [app-db]]
            [rock-paper-scissors.cofx :as cofx]
            [rock-paper-scissors.db :as db]
            [rock-paper-scissors.db.specs :as db.specs]
            [rock-paper-scissors.events :as sut]
            [rock-paper-scissors.rps :as rps]
            [rock-paper-scissors.rps.specs]
            [rock-paper-scissors.subs :as subs]))

(t/use-fixtures
  :once {:before #(stest/instrument)})

(defn test-fixtures []
  (re-frame/reg-fx
    :db
    (fn [value]
      (when-not (s/valid? ::db.specs/db value)
        (throw (ex-info "db spec check failed" (s/explain-data ::db.specs/db value))))
      (if-not (identical? @app-db value)
        (reset! app-db value)))))

(t/deftest test-initialize-db
  (re-frame.test/run-test-sync
   (test-fixtures)
   (re-frame/dispatch [::sut/initialize-db])
   (t/is ::db/start @(re-frame/subscribe [::subs/scene]))
   (t/is {:you ::rps/rock
          :enemy ::rps/rock}
         @(re-frame/subscribe [::subs/you-enemy]))))

(t/deftest test-next-game
  (re-frame.test/run-test-sync
   (test-fixtures)
   (re-frame/dispatch [::sut/initialize-db])
   (re-frame/dispatch [::sut/next-game])
   (t/is ::db/now-playing @(re-frame/subscribe [::subs/scene]))))

(t/deftest test-select-your-hand
  (re-frame.test/run-test-sync
   (test-fixtures)
   (re-frame/reg-cofx
     ::cofx/select-enemy-hand
     (fn [cofx _]
       (assoc cofx
              :enemy-hand ::rps/rock)))
   (re-frame/dispatch [::sut/initialize-db])
   (t/testing "draw"
     (re-frame/dispatch [::sut/next-game])
     (re-frame/dispatch [::sut/select-your-hand ::rps/rock])
     (t/is ::db/over @(re-frame/subscribe [::subs/scene]))
     (t/is "rock(YOU) VS rock(ENEMY)" @(re-frame/subscribe [::subs/you-enemy-hands]))
     (t/is ::rps/draw @(re-frame/subscribe [::subs/fight-result]))
     (t/is "gray" @(re-frame/subscribe [::subs/result-color])))
   (t/testing "win"
     (re-frame/dispatch [::sut/next-game])
     (re-frame/dispatch [::sut/select-your-hand ::rps/paper])
     (t/is ::db/over @(re-frame/subscribe [::subs/scene]))
     (t/is "paper(YOU) VS rock(ENEMY)" @(re-frame/subscribe [::subs/you-enemy-hands]))
     (t/is ::rps/win @(re-frame/subscribe [::subs/fight-result]))
     (t/is "red" @(re-frame/subscribe [::subs/result-color])))
   (t/testing "lose"
     (re-frame/dispatch [::sut/next-game])
     (re-frame/dispatch [::sut/select-your-hand ::rps/scissors])
     (t/is ::db/over @(re-frame/subscribe [::subs/scene]))
     (t/is "scissors(YOU) VS rock(ENEMY)" @(re-frame/subscribe [::subs/you-enemy-hands]))
     (t/is ::rps/lose @(re-frame/subscribe [::subs/fight-result]))
     (t/is "blue" @(re-frame/subscribe [::subs/result-color])))))
```

# まとめ

- re-frameを利用するとシンプルなデータと純粋な関数の組み合わせでClojure(Script)らしくフロントエンドの状態管理ができる
    - 本記事で開発の流れとともに基本的な機能の紹介を試みたが、初期の学習コストは低くないかも
    - 詳しくは公式ドキュメント[docs](https://github.com/day8/re-frame/tree/master/docs)を読もう
- clojure.specは控えめに開発時のみ利用する方針でも十分役に立ちそう
- ドメインロジックとre-frameのeventを中心にテストする方針が良さそう
- ClojureScript楽しい＞ω＜/
- re-frame楽しい＞ω＜/

# Further Reading

## ClojureScript

- [Clojure/ClojureScript関連リンク集 > Clojure公式](https://qiita.com/lagenorhynque/items/68c314c288b75a9492ba#clojure%E5%85%AC%E5%BC%8F)

## Reagent & re-frame

- [ClojureのDuctとClojureScriptのre-frameによるREST API + SPA開発入門](https://qiita.com/lagenorhynque/items/38537fa91300e0ac0070)
- [Clojure/ClojureScript関連リンク集 > Webフロントエンド (ClojureScript)](https://qiita.com/lagenorhynque/items/68c314c288b75a9492ba#web%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89-clojurescript)

## clojure.spec

- [Clojure/ClojureScript関連リンク集 > 標準ライブラリ](https://qiita.com/lagenorhynque/items/68c314c288b75a9492ba#%E6%A8%99%E6%BA%96%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA)

## 開発環境

- [Clojure/ClojureScript関連リンク集 > 開発環境](https://qiita.com/lagenorhynque/items/68c314c288b75a9492ba#%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83)