Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
re-frame à la spec
Search
Kent OHASHI
March 29, 2018
Programming
0
43
re-frame à la spec
Introduction to ClojureScript SPA framework "re-frame" and its integration with clojure.spec.
Kent OHASHI
March 29, 2018
Tweet
Share
More Decks by Kent OHASHI
See All by Kent OHASHI
do Notation Equivalents in JVM languages: Scala, Kotlin, Clojure
lagenorhynque
0
3
Exploring Collections in JVM Languages through Internals of map Function
lagenorhynque
0
11
Kotlin Meets Data-Oriented Programming
lagenorhynque
0
12
Introduction to Tree Representations in RDB 2024
lagenorhynque
0
27
Boundary between Mutability and Immutability
lagenorhynque
0
39
Learning Modern Web API Styles from IDL: REST, GraphQL, gRPC
lagenorhynque
0
72
Team Geek Revisited
lagenorhynque
0
56
Scala vs Clojure?: The Rise and Fall of Functional Languages in Opt Technologies
lagenorhynque
0
110
Exploring Immutable Persistent World with Clojure Collections
lagenorhynque
0
110
Other Decks in Programming
See All in Programming
タクシーアプリ『GO』のリアルタイムデータ分析基盤における機械学習サービスの活用
mot_techtalk
6
1.6k
CSC509 Lecture 11
javiergs
PRO
0
180
Vapor Revolution
kazupon
2
570
Why Jakarta EE Matters to Spring - and Vice Versa
ivargrimstad
0
1.3k
Remix on Hono on Cloudflare Workers
yusukebe
1
310
最新TCAキャッチアップ
0si43
0
210
Better Code Design in PHP
afilina
PRO
0
130
見せてあげますよ、「本物のLaravel批判」ってやつを。
77web
7
7.8k
型付き API リクエストを実現するいくつかの手法とその選択 / Typed API Request
euxn23
8
2.4k
Make Impossible States Impossibleを 意識してReactのPropsを設計しよう
ikumatadokoro
0
300
3 Effective Rules for Using Signals in Angular
manfredsteyer
PRO
1
110
OnlineTestConf: Test Automation Friend or Foe
maaretp
0
120
Featured
See All Featured
What's new in Ruby 2.0
geeforr
343
31k
What's in a price? How to price your products and services
michaelherold
243
12k
Mobile First: as difficult as doing things right
swwweet
222
8.9k
Making the Leap to Tech Lead
cromwellryan
133
8.9k
Unsuck your backbone
ammeep
668
57k
JavaScript: Past, Present, and Future - NDC Porto 2020
reverentgeek
47
5k
A better future with KSS
kneath
238
17k
KATA
mclloyd
29
14k
A Philosophy of Restraint
colly
203
16k
個人開発の失敗を避けるイケてる考え方 / tips for indie hackers
panda_program
93
17k
Automating Front-end Workflow
addyosmani
1366
200k
ReactJS: Keep Simple. Everything can be a component!
pedronauck
665
120k
Transcript
re-frame à la spec
Self-introduction /laʒenɔʁɛ̃k/ カマイルカ lagénorhynque (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 inspired by lagenorhynque/rock-paper-scissors Elm開発における思考フロー
demo or from Emacs: cider-jack-in-clojurescript $ git clone
[email protected]
:lagenorhynque/rock-paper-scissors.git $
cd rock-paper-scissors $ lein figwheel dev
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 (React) (Angular) Elm Architecture Redux ngrx
React Reagent JSX (React element) Hiccup-like DSL React component ClojureScript
function props args of component function state reagent.core/atom cf. Reagent 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 subscription for ::you-enemy-hands (re-frame/reg-sub ::you-enemy (fn [db
_] (select-keys db [:you :enemy]))) src/cljs/rock_paper_scissors/subs.cljs#L10-L13 (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
"dev" :source-paths ["src/cljs" "specs/cljs"] ,,,} {:id "min" :source-paths ["src/cljs"] ,,,} {:id "test" :source-paths ["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#L11- L22
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#L6- L12
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#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- (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
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 : A minimalistic ClojureScript interface to React.js reagent-project/reagent Guide
to Reagent ClojureScript & ReagentでReact⼊⾨してみた - Qiita
re-frame : A Reagent Framework For Writing SPAs, in Clojurescript.
: Cross platform (cljs and clj) utilities for testing re-frame applications : A debugging dashboard for re-frame epochs. Comes with free x-ray glasses. Day8/re-frame Day8/re-frame-test Day8/re-frame-10x 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