312 Views
March 29, 18
スライド概要
Introduction to ClojureScript SPA framework "re-frame" and its integration with clojure.spec.
「楽しく楽にcoolにsmartに」を理想とするprogrammer/philosopher/liberalist/realist。 好きな言語はClojure, Haskell, Python, English, français, русский。 読書、プログラミング、語学、法学、数学が大好き! イルカと海も大好き🐬
re-frame à la spec
Self-introduction 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])
1. Example App 2. Introduction to re-frame 3. Integration with clojure.spec
Example App rock-paper-scissors game
source code lagenorhynque/rock-paper-scissors inspired by Elm開発における思考フロー
demo $ git clone [email protected]:lagenorhynque/rock-paper-scissors.git $ cd rock-paper-scissors $ lein figwheel dev or from Emacs: cider-jack-in-clojurescript
Game start
Select your hand
Result: lose
Select your hand
Result: draw
Select your hand
Result: win
Introduction to re-frame
re-frame A Reagent Framework For Writing SPAs, in Clojurescript.
features based on Reagent data-oriented design purely functional for the most part
similar frameworks Elm Architecture Redux (React) ngrx (Angular)
Reagent 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⼊⾨してみた
re-frame's data ow (a.k.a. "dominoes") 1. event dispatch 2. event handling 3. effect handling 4. query 5. view 6. DOM
1. event dispatch 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
2. event handling 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
3. effect handling 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
4. query subscription for ::scene (re-frame/reg-sub ::scene (fn [db _] (:scene db))) src/cljs/rock_paper_scissors/subs.cljs#L5-L8
5. view 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
6. DOM
1'. event dispatch
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
2'. event handling 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
3'. effect handling 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
4'. query subscription for ::scene (re-frame/reg-sub ::scene (fn [db _] (:scene db))) src/cljs/rock_paper_scissors/subs.cljs#L5-L8
5'. view 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
6'. DOM
1''. event dispatch
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
2''. event handling 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
3''. effect handling 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
4''. query 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
5''. view 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
6''. DOM
Integration with clojure.spec
speccing policy split directories/namespaces for specs use clojure.spec in development only spec domain logic spec db data
split directories/namespaces for specs 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
use clojure.spec in development only
cljsbuild settings
:cljsbuild
{:builds
[{:id
:source-paths
,,,}
{:id
:source-paths
,,,}
{:id
:source-paths
,,,}
]}
"dev"
["src/cljs" "specs/cljs"]
"min"
["src/cljs"]
"test"
["src/cljs" "specs/cljs" "test/cljs"]
project.clj#L40-L70
spec domain logic 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#L11L22
spec db data (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#L6L12
testing policy test domain logic test events via dispatch test subscriptions via subscribe do not test views
test domain logic 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
test events and subscriptions
(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#L16L26
(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#L28L35
(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(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#L44L73
re-frame x clojure.spec 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!
Further Reading example code lagenorhynque/rock-paper-scissors
Reagent reagent-project/reagent: A minimalistic ClojureScript interface to React.js Guide to Reagent ClojureScript & ReagentでReact⼊⾨してみた Qiita
re-frame 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. Re-frame: The Guide to Building Blocks
clojure.spec clojure.spec - Rationale and Overview clojure.spec - 論理的根拠と概要 spec Guide やってみる!clojure.spec Spectacular Future with clojure.spec