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
59
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
KotlinでミニマルなResult実装による関数型エラーハンドリング
lagenorhynque
0
10
Functional Calisthenics in Kotlin: Kotlinで「関数型エクササイズ」を実践しよう
lagenorhynque
0
140
関数型言語テイスティング: Haskell, Scala, Clojure, Elixirを比べて味わう関数型プログラミングの旨さ
lagenorhynque
1
120
純LISPから考える関数型言語のプリミティブ: Clojure, Elixir, Haskell, Scala
lagenorhynque
1
150
From Scala/Clojure to Kotlin
lagenorhynque
0
64
TDD with RDD: Changed Developer Experience through Clojure/Lisp REPLs
lagenorhynque
0
91
My Favourite Book in 2024: Get Rid of Your Japanese Accent
lagenorhynque
1
130
do Notation Equivalents in JVM languages: Scala, Kotlin, Clojure
lagenorhynque
0
96
map関数の内部実装から探るJVM言語のコレクション: Scala, Kotlin, Clojureコレクションの基本的な設計を理解しよう
lagenorhynque
0
93
Other Decks in Programming
See All in Programming
JEP 496 と JEP 497 から学ぶ耐量子計算機暗号入門 / Learning Post-Quantum Crypto Basics from JEP 496 & 497
mackey0225
2
460
Rails Girls Sapporo 2ndの裏側―準備の日々から見えた、私が得たもの / SAPPORO ENGINEER BASE #11
lemonade_37
2
190
モビリティSaaSにおけるデータ利活用の発展
nealle
0
590
CloudNative Days Winter 2025: 一週間で作る低レイヤコンテナランタイム
ternbusty
7
1.7k
Flutterアプリ運用の現場で役立った監視Tips 5選
ostk0069
1
510
FlutterKaigi 2025 システム裏側
yumnumm
0
1.2k
歴史から学ぶ「Why PHP?」 PHPを書く理由を改めて理解する / Learning from History: “Why PHP?” Rediscovering the Reasons for Writing PHP
seike460
PRO
0
160
『実践MLOps』から学ぶ DevOps for ML
nsakki55
2
470
JJUG CCC 2025 Fall: Virtual Thread Deep Dive
ternbusty
3
480
仕様がそのままテストになる!Javaで始める振る舞い駆動開発
ohmori_yusuke
8
4.6k
2025 컴포즈 마법사
jisungbin
0
150
PHPライセンス変更の議論を通じて学ぶOSSライセンスの基礎
matsuo_atsushi
0
170
Featured
See All Featured
JavaScript: Past, Present, and Future - NDC Porto 2020
reverentgeek
52
5.7k
Templates, Plugins, & Blocks: Oh My! Creating the theme that thinks of everything
marktimemedia
31
2.6k
Side Projects
sachag
455
43k
The Art of Delivering Value - GDevCon NA Keynote
reverentgeek
16
1.8k
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
32
1.8k
実際に使うSQLの書き方 徹底解説 / pgcon21j-tutorial
soudai
PRO
192
56k
Large-scale JavaScript Application Architecture
addyosmani
514
110k
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
31
3k
A designer walks into a library…
pauljervisheath
210
24k
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
48
9.8k
ReactJS: Keep Simple. Everything can be a component!
pedronauck
666
130k
Faster Mobile Websites
deanohume
310
31k
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