lagénorhynque /laʒenɔʁɛ̃k/ カマイルカ
(defprofile lagénorhynque
:name "Kent OHASHI"
:languages [Clojure Haskell Python Scala
English français Deutsch русский]
:interests [programming language-learning mathematics]
:contributing [github.com/japan-clojurians/clojure-site-ja])-
Example App
-
Introduction to re-frame
-
Integration with clojure.spec
rock-paper-scissors game
-
lagenorhynque/rock-paper-scissors
- inspired by Elm開発における思考フロー
$ git clone git@github.com:lagenorhynque/rock-paper-scissors.git
$ cd rock-paper-scissors
$ lein figwheel devor
from Emacs: cider-jack-in-clojurescript
Game start
Select your hand
Result: lose 😇
Select your hand
Result: draw 🤔
Select your hand
Result: win 😆
A Reagent Framework For Writing SPAs, in Clojurescript.
-
based on Reagent
-
data-oriented design
-
purely functional for the most part
| React | Reagent |
|---|---|
| JSX (React element) | Hiccup-like DSL |
| React component | ClojureScript function |
| props | args of component function |
| state | reagent.core/atom |
cf. ClojureScript & ReagentでReact入門してみた
(a.k.a. "dominoes")
-
event dispatch
-
event handling
-
effect handling
-
query
-
view
-
DOM
- dispatch synchronously to
[::events/initialize-db]
(defn ^:export init []
(re-frame/dispatch-sync [::events/initialize-db])
(dev-setup)
(mount-root))src/cljs/rock_paper_scissors/core.cljs#L19-L22
- event handler for
::initialize-db
(re-frame/reg-event-db
::initialize-db
(fn [_ _]
db/default-db))src/cljs/rock_paper_scissors/events.cljs#L6-L9
- effect handler for
:db
(reg-fx
:db
(fn [value]
(if-not (identical? @app-db value)
(reset! app-db value))))src/re_frame/fx.cljc#L162-L166
- subscription for
::scene
(re-frame/reg-sub
::scene
(fn [db _]
(:scene db)))src/cljs/rock_paper_scissors/subs.cljs#L5-L8
- subscribe to
[::subs/scene]
(defn main-panel []
(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"]]))src/cljs/rock_paper_scissors/views.cljs#L26-L33
- dispatch to
[::events/next-game]
(defn start-button [label]
[:input {:type "button"
:on-click #(re-frame/dispatch [::events/next-game])
:value label}])src/cljs/rock_paper_scissors/views.cljs#L8-L11
- event handler for
::next-game
(re-frame/reg-event-db
::next-game
(fn [db _]
(assoc db :scene ::db/now-playing)))src/cljs/rock_paper_scissors/events.cljs#L11-L14
- effect handler for
:db
(reg-fx
:db
(fn [value]
(if-not (identical? @app-db value)
(reset! app-db value))))src/re_frame/fx.cljc#L162-L166
- subscription for
::scene
(re-frame/reg-sub
::scene
(fn [db _]
(:scene db)))src/cljs/rock_paper_scissors/subs.cljs#L5-L8
- subscribe to
[::subs/scene]
(defn main-panel []
(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"]]))src/cljs/rock_paper_scissors/views.cljs#L26-L33
- dispatch to
[::events/select-your-hand h]
(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])])src/cljs/rock_paper_scissors/views.cljs#L13-L19
- event handler for
::events/select-your-hand
(re-frame/reg-event-fx
::select-your-hand
[(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)}))src/cljs/rock_paper_scissors/events.cljs#L16-L23
- coeffect handler for
::select-enemy-hand
(re-frame/reg-cofx
::select-enemy-hand
(fn [cofx _]
(assoc cofx
:enemy-hand (rps/->hand (rand-int 3)))))src/cljs/rock_paper_scissors/cofx.cljs#L5-L9
- effect handler for
:db
(reg-fx
:db
(fn [value]
(if-not (identical? @app-db value)
(reset! app-db value))))src/re_frame/fx.cljc#L162-L166
- subscription for
::scene
(re-frame/reg-sub
::scene
(fn [db _]
(:scene db)))src/cljs/rock_paper_scissors/subs.cljs#L5-L8
- subscription for
::you-enemy
(re-frame/reg-sub
::you-enemy
(fn [db _]
(select-keys db [:you :enemy])))src/cljs/rock_paper_scissors/subs.cljs#L10-L13
- subscription for
::you-enemy-hands
(re-frame/reg-sub
::you-enemy-hands
:<- [::you-enemy]
(fn [{:keys [you enemy]} _]
(str (name you) "(YOU) VS " (name enemy) "(ENEMY)")))src/cljs/rock_paper_scissors/subs.cljs#L15-L19
- subscription for
::fight-result
(re-frame/reg-sub
::fight-result
:<- [::you-enemy]
(fn [{:keys [you enemy]} _]
(rps/fight you enemy)))src/cljs/rock_paper_scissors/subs.cljs#L21-L25
- subscription for
::result-color
(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/subs.cljs#L27-L34
- subscribe to
[::subs/scene] - subscribe to
[::subs/you-enemy-hands]
(defn main-panel []
(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"]]))src/cljs/rock_paper_scissors/views.cljs#L26-L33
- subscribe to
[::subs/fight-result] - subscribe to
[::subs/result-color]
(defn result []
(let [r @(re-frame/subscribe [::subs/fight-result])]
[:h1 {:style {:color @(re-frame/subscribe
[::subs/result-color])}}
r]))src/cljs/rock_paper_scissors/views.cljs#L21-L24
-
split directories/namespaces for specs
-
use clojure.spec in development only
-
spec domain logic
-
spec db data
directory structure
rock-paper-scissors
├── specs
│ └── cljs
│ └── rock_paper_scissors
│ └── *
│ └── specs.cljs
├── src
│ └── cljs
│ └── rock_paper_scissors
│ └── *.cljs
└── test
└── cljs
└── rock_paper_scissors
├── *_test.cljs
└── runner.cljs
cljsbuild settings
:cljsbuild
{:builds
[{:id "dev"
:source-paths ["src/cljs" "specs/cljs"]
,,,}
{:id "min"
:source-paths ["src/cljs"]
,,,}
{:id "test"
:source-paths ["src/cljs" "specs/cljs" "test/cljs"]
,,,}
]}specs for rock-paper-scissors data
(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})specs/cljs/rock_paper_scissors/rps/specs.cljs#L5-L9
specs for rock-paper-scissors functions
(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)specs/cljs/rock_paper_scissors/rps/specs.cljs#L11-L22
(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]))specs/cljs/rock_paper_scissors/db/specs.cljs#L6-L12
-
test domain logic
-
test events via
dispatch -
test subscriptions via
subscribe- do not test views
example-based tests with specs instrumented
(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)))))test/cljs/rock_paper_scissors/rps_test.cljs#L10-L17
property-based tests using specs
(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)))))test/cljs/rock_paper_scissors/rps_test.cljs#L33-L38
(t/use-fixtures ; instrument specs
:once {:before #(stest/instrument)})
(defn test-fixtures []
(re-frame/reg-fx ; mock :db effect
:db
(fn [value] ; validate db with specs
(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)))))test/cljs/rock_paper_scissors/events_test.cljs#L16-L26
(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]))))test/cljs/rock_paper_scissors/events_test.cljs#L28-L35
(t/deftest test-select-your-hand
(re-frame.test/run-test-sync
(test-fixtures)
(re-frame/reg-cofx ; mock ::cofx/select-enemy-hand coeffect
::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])))))test/cljs/rock_paper_scissors/events_test.cljs#L44-L73
- unobtrusive use of clojure.spec
-
only in development
-
in separate directory/file/namespace
-
focus on domain logic and db
-
A little effort to write specs and tests can make our ClojureScript frontend life much happier!
-
reagent-project/reagent: A minimalistic ClojureScript interface to React.js
-
Day8/re-frame: A Reagent Framework For Writing SPAs, in Clojurescript.
-
Day8/re-frame-test: Cross platform (cljs and clj) utilities for testing re-frame applications
-
Day8/re-frame-10x: A debugging dashboard for re-frame epochs. Comes with free x-ray glasses.















