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
56
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
関数型言語テイスティング: Haskell, Scala, Clojure, Elixirを比べて味わう関数型プログラミングの旨さ
lagenorhynque
1
92
純LISPから考える関数型言語のプリミティブ: Clojure, Elixir, Haskell, Scala
lagenorhynque
1
96
From Scala/Clojure to Kotlin
lagenorhynque
0
43
TDD with RDD: Changed Developer Experience through Clojure/Lisp REPLs
lagenorhynque
0
75
My Favourite Book in 2024: Get Rid of Your Japanese Accent
lagenorhynque
1
110
do Notation Equivalents in JVM languages: Scala, Kotlin, Clojure
lagenorhynque
0
79
map関数の内部実装から探るJVM言語のコレクション: Scala, Kotlin, Clojureコレクションの基本的な設計を理解しよう
lagenorhynque
0
73
Kotlin Meets Data-Oriented Programming
lagenorhynque
0
66
Introduction to Tree Representations in RDB 2024
lagenorhynque
0
98
Other Decks in Programming
See All in Programming
GitHubとGitLabとAWS CodePipelineでCI/CDを組み比べてみた
satoshi256kbyte
4
240
モバイルアプリからWebへの横展開を加速した話_Claude_Code_実践術.pdf
kazuyasakamoto
0
330
print("Hello, World")
eddie
2
530
Laravel Boost 超入門
fire_arlo
3
220
Android 16 × Jetpack Composeで縦書きテキストエディタを作ろう / Vertical Text Editor with Compose on Android 16
cc4966
1
230
個人軟體時代
ethanhuang13
0
320
Updates on MLS on Ruby (and maybe more)
sylph01
1
180
開発チーム・開発組織の設計改善スキルの向上
masuda220
PRO
20
11k
請來的 AI Agent 同事們在寫程式時,怎麼用 pytest 去除各種幻想與盲點
keitheis
0
120
知っているようで知らない"rails new"の世界 / The World of "rails new" You Think You Know but Don't
luccafort
PRO
1
160
奥深くて厄介な「改行」と仲良くなる20分
oguemon
1
540
testingを眺める
matumoto
1
140
Featured
See All Featured
[Rails World 2023 - Day 1 Closing Keynote] - The Magic of Rails
eileencodes
36
2.5k
Code Review Best Practice
trishagee
70
19k
ピンチをチャンスに:未来をつくるプロダクトロードマップ #pmconf2020
aki_iinuma
126
53k
Why You Should Never Use an ORM
jnunemaker
PRO
59
9.5k
The Cult of Friendly URLs
andyhume
79
6.6k
Improving Core Web Vitals using Speculation Rules API
sergeychernyshev
18
1.1k
The Myth of the Modular Monolith - Day 2 Keynote - Rails World 2024
eileencodes
26
3k
ReactJS: Keep Simple. Everything can be a component!
pedronauck
667
120k
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
48
9.7k
Cheating the UX When There Is Nothing More to Optimize - PixelPioneers
stephaniewalter
285
13k
Into the Great Unknown - MozCon
thekraken
40
2k
No one is an island. Learnings from fostering a developers community.
thoeni
21
3.4k
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