Slide 1

Slide 1 text

Spectacular Future with clojure.spec

Slide 2

Slide 2 text

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])

Slide 3

Slide 3 text

「Clojureをプロダクトに導⼊した話」

Slide 4

Slide 4 text

Clojure

Slide 5

Slide 5 text

Contents 1. Clojure Quick Intro 2. New Feature: clojure.spec

Slide 6

Slide 6 text

Clojure Quick Intro

Slide 7

Slide 7 text

Clojure Lisp S式, マクロ, etc. REPL駆動開発 関数型プログラミング⾔語 動的⾔語 JVM⾔語 (cf. ClojureScript) ⇒ シンプルで強⼒な⾔語

Slide 8

Slide 8 text

リテラル type example string "abc" character \a number 1, 2.0, 3N, 4.5M, 6/7, 8r10 boolean true, false nil nil keyword :a, :user/a, ::a, ::x/a symbol 'a, 'user/a, `a, `x/a

Slide 9

Slide 9 text

type example list '(1 2 3), '(+ 1 2 3) vector [1 2 3] set #{1 2 3} map {:a 1 :b 2}, #:user{:a 1 :b 2}, #::{:a 1 :b 2}, #::x{:a 1 :b 2} function (fn [x] (* x x))

Slide 10

Slide 10 text

シンタックス オペレータ 関数 マクロ 特殊形式 (op arg1 arg2 ... argn)

Slide 11

Slide 11 text

New Feature: clojure.spec

Slide 12

Slide 12 text

例: 直⽅体の体積計算 直⽅体の体積 = 辺a × 辺b × 辺c user> (defn cuboid-volume [{:keys [side-a side-b side-c]}] (* side-a side-b side-c)) #'user/cuboid-volume user> (cuboid-volume {:side-a 1 :side-b 2 :side-c 3}) 6

Slide 13

Slide 13 text

エラー Javaのスタックトレースが\(^o^)/…… ;; マップの値の型が数値ではなく⽂字列 user> (cuboid-volume {:side-a 1 :side-b "2" :side-c 3}) ClassCastException java.lang.String cannot be cast to java.lang. Number clojure.lang.Numbers.multiply (Numbers.java:148) ;; マップのキー名でtypo user> (cuboid-volume {:side-a 1 :side-d 2 :side-c 3}) NullPointerException clojure.lang.Numbers.ops (Numbers.java:10 18) ;; マップに必須のキーがない user> (cuboid-volume {:side-a 1 :side-c 3}) NullPointerException clojure.lang.Numbers.ops (Numbers.java:10 18)

Slide 14

Slide 14 text

問題点 動的⾔語なのでコンパイル時に引数の型の不整合が 検出されない 少なくとも実⾏時に分かりやすいエラーになって ほしい マップのkey-valueに対するチェックがない 動的なデータを柔軟に表現したい

Slide 15

Slide 15 text

cf. 漸進的型付け(gradual typing) Clojureに静的型システムを追加 コンパイル時に型チェック core.typed (require '[clojure.core.typed :as t]) (t/ann cuboid-volume [(t/HMap :mandatory {:side-a t/Num :side-b t/Num :side-c t/Num}) :-> t/Num]) (defn cuboid-volume [{:keys [side-a side-b side-c]}] (* side-a side-b side-c))

Slide 16

Slide 16 text

cf. データ記述/バリデーションDSL 独⾃DSLでデータ構造を表現 実⾏時にバリデーション schema (require '[schema.core :as s]) (s/defn cuboid-volume :- s/Num [{:keys [side-a side-b side-c]} :- {:side-a s/Num :side-b s/Num :side-c s/Num}] (* side-a side-b side-c))

Slide 17

Slide 17 text

clojure.spec 述語(predicate)を組み合わせて仕様(spec)を書いて ドキュメント バリデーション 詳細なエラー報告 パースと分配束縛 データ⽣成 プロパティベーストテスト などを実現する仕組み

Slide 18

Slide 18 text

dependency REPL [org.clojure/clojure "1.9.0-beta1"] user> (require '[clojure.spec.alpha :as s] '[clojure.spec.test.alpha :as stest] '[clojure.spec.gen.alpha :as gen]) nil

Slide 19

Slide 19 text

「⻑さ」をspecで表現 でキーワード :user/length に述語 number? を登録 user> (s/def ::length number?) :user/length user> (doc ::length) ------------------------- :user/length Spec number? nil s/def

Slide 20

Slide 20 text

「⻑さ」に⼀致する値を調べる で ::length のspecに具体的な値が ⼀致するか確認 ⼀致しなければ ::s/invalid user> (s/conform ::length 3) 3 user> (s/conform ::length "3") :clojure.spec.alpha/invalid s/conform

Slide 21

Slide 21 text

⼀致しない原因を調べる で⼀致しない詳細原因を確認 ここでは 値 "3" がspec :user/length の述語 number? に不⼀致 user> (s/explain ::length "3") val: "3" fails spec: :user/length predicate: number? :clojure.spec.alpha/spec :user/length :clojure.spec.alpha/value "3" nil s/explain

Slide 22

Slide 22 text

「⻑さ」のサンプルを⽣成 で 互換なジェネレータを取得 でサンプルを⽣成 user> (s/gen ::length) #clojure.test.check.generators.Generator{:gen #function[clojure. test.check.generators/such-that/fn--13745]} user> (gen/sample (s/gen ::length)) (0.5 -1.0 -1 0.5 3.25 0 -3 0.6875 0.25 0) s/gen test.check gen/sample

Slide 23

Slide 23 text

「⻑さ」に制約を加える 論理演算⼦で制約を追加 は論理積 cf. user> (s/def ::length (s/and number? pos?)) :user/length user> (doc ::length) ------------------------- :user/length Spec (and number? pos?) nil user> (gen/sample (s/gen ::length)) (0.5 3.0 0.5 1.375 1.6875 3.0 0.625 1.25 0.41015625 0.25) s/and s/or

Slide 24

Slide 24 text

「⻑さ」で辺a, b, cを表現 user> (s/def ::side-a ::length) :user/side-a user> (doc ::side-a) ------------------------- :user/side-a Spec (and number? pos?) nil user> (s/def ::side-b ::length) :user/side-b user> (s/def ::side-c ::length) :user/side-c

Slide 25

Slide 25 text

辺a, b, cを持つマップとして直⽅体 を表現 で必須のキーを持つマップを表現 user> (s/def ::cuboid (s/keys :req [::side-a ::side-b ::side-c]) ) :user/cuboid user> (doc ::cuboid) ------------------------- :user/cuboid Spec (keys :req [:user/side-a :user/side-b :user/side-c]) nil s/keys

Slide 26

Slide 26 text

直⽅体に⼀致する値を調べる user> (s/conform ::cuboid #::{:side-a 1 :side-b 2 :side-c 3}) #:user{:side-a 1, :side-b 2, :side-c 3} user> (s/conform ::cuboid #::{:side-a 1 :side-b "2" :side-c 3}) :clojure.spec.alpha/invalid

Slide 27

Slide 27 text

⼀致しない原因を調べる :user/side-b の値 "2" がspec :user/length の述語 number? に不⼀致 マップのキーに対応するspecもチェックされる user> (s/explain ::cuboid #::{:side-a 1 :side-b "2" :side-c 3}) In: [:user/side-b] val: "2" fails spec: :user/length at: [:user/ side-b] predicate: number? :clojure.spec.alpha/spec :user/cuboid :clojure.spec.alpha/value #:user{:side-a 1, :side-b "2", :side- c 3} nil

Slide 28

Slide 28 text

値 #:user{:side-a 1, :side-d 2, :side-c 3} がspec :user/cuboid の述語 (contains? % :user/side-b) に不⼀致 user> (s/explain ::cuboid #::{:side-a 1 :side-d 2 :side-c 3}) val: #:user{:side-a 1, :side-d 2, :side-c 3} fails spec: :user/c uboid predicate: (contains? % :user/side-b) :clojure.spec.alpha/spec :user/cuboid :clojure.spec.alpha/value #:user{:side-a 1, :side-d 2, :side-c 3} nil

Slide 29

Slide 29 text

値 #:user{:side-a 1, :side-c 3} がspec :user/cuboid の述語 (contains? % :user/side-b) に不⼀致 user> (s/explain ::cuboid #::{:side-a 1 :side-c 3}) val: #:user{:side-a 1, :side-c 3} fails spec: :user/cuboid predi cate: (contains? % :user/side-b) :clojure.spec.alpha/spec :user/cuboid :clojure.spec.alpha/value #:user{:side-a 1, :side-c 3} nil

Slide 30

Slide 30 text

直⽅体のサンプルを⽣成 複合的なデータのサンプルも⽣成できる user> (gen/sample (s/gen ::cuboid)) (#:user{:side-a 0.5, :side-b 1.0, :side-c 0.625} #:user{:side-a 0.5, :side-b 1.5, :side-c 2} #:user{:side-a 2.0, :side-b 4, :sid e-c 1} #:user{:side-a 1.5, :side-b 1.0, :side-c 1} #:user{:side- a 0.5, :side-b 12, :side-c 1.125} #:user{:side-a 49, :side-b 0.3 59375, :side-c 0.765625} #:user{:side-a 2, :side-b 1, :side-c 1. 25} #:user{:side-a 3.0, :side-b 0.5, :side-c 2.0} #:user{:side-a 1.3125, :side-b 1.0, :side-c 1.0} #:user{:side-a 5.265625, :sid e-b 1.0625, :side-c 2.73828125})

Slide 31

Slide 31 text

直⽅体の体積計算の仕様を表現 で関数 cuboid-volume の引数と戻り値 に対するspecを登録 引数: ::cuboid 1要素のシーケンス は正規表現演算⼦ cf. , , , , 戻り値: 数値 user> (s/fdef cuboid-volume :args (s/cat :cuboid ::cuboid) :ret number?) user/cuboid-volume s/fdef s/cat s/* s/+ s/? s/& s/alt

Slide 32

Slide 32 text

仕様を満たすように関数を実装 user> (defn cuboid-volume [{::keys [side-a side-b side-c]}] (* side-a side-b side-c)) #'user/cuboid-volume

Slide 33

Slide 33 text

引数に対するチェックを有効化 stest/instrument user> (doc cuboid-volume) ------------------------- user/cuboid-volume ([#:user{:keys [side-a side-b side-c]}]) Spec args: (cat :cuboid :user/cuboid) ret: number? nil user> (stest/instrument) [user/cuboid-volume]

Slide 34

Slide 34 text

引数のspecを満たす値に適⽤すると期待した計算 結果が得られる user> (cuboid-volume #::{:side-a 1 :side-b 2 :side-c 3}) 6

Slide 35

Slide 35 text

パス [0 :user/side-b] の値 "2" がspec :user/length の述語 number? に不⼀致 ⇒ 例外 user> (cuboid-volume #::{:side-a 1 :side-b "2" :side-c 3}) ExceptionInfo Call to #'user/cuboid-volume did not conform to sp ec: In: [0 :user/side-b] val: "2" fails spec: :user/length at: [:arg s :cuboid :user/side-b] predicate: number? :clojure.spec.alpha/spec #object[clojure.spec.alpha$regex_spec_ impl$reify__1200 0x57e771b6 "clojure.spec.alpha$regex_spec_impl$ reify__1200@57e771b6"] :clojure.spec.alpha/value (#:user{:side-a 1, :side-b "2", :side -c 3}) :clojure.spec.alpha/args (#:user{:side-a 1, :side-b "2", :side- c 3}) :clojure.spec.alpha/failure :instrument :clojure.spec.test.alpha/caller {:file "form-init41134222269549 81451.clj", :line 188, :var-scope user/eval14130} clojure.core/ex-info (core.clj:4744)

Slide 36

Slide 36 text

関数の動作確認 で引数のspecを満たすランダム な値で動作確認 結果はベクター [引数 戻り値] のシーケンス user> (s/exercise-fn `cuboid-volume) ([(#:user{:side-a 2.0, :side-b 0.5, :side-c 0.5}) 0.5] [(#:user{ :side-a 0.75, :side-b 1.5, :side-c 0.75}) 0.84375] [(#:user{:sid e-a 2.0, :side-b 1, :side-c 1.75}) 3.5] [(#:user{:side-a 4, :sid e-b 4, :side-c 2}) 32] [(#:user{:side-a 2, :side-b 6, :side-c 1. 0}) 12.0] [(#:user{:side-a 1, :side-b 3, :side-c 6}) 18] [(#:use r{:side-a 20, :side-b 1.0, :side-c 99}) 1980.0] [(#:user{:side-a 12, :side-b 890, :side-c 1.25}) 13350.0] [(#:user{:side-a 4, :s ide-b 0.99609375, :side-c 2.0}) 7.96875] [(#:user{:side-a 3, :si de-b 6.0, :side-c 9}) 162.0]) s/exercise-fn

Slide 37

Slide 37 text

⾃動プロパティベーストテスト stest/check user> (stest/check `cuboid-volume) ({:spec #object[clojure.spec.alpha$fspec_impl$reify__1215 0x765acd43 "c :cause "integer overflow" :via [{:type java.lang.ArithmeticException :message "integer overflow" :at [clojure.lang.Numbers throwIntOverflow "Numbers.java" 1526]}] :trace [[clojure.lang.Numbers throwIntOverflow "Numbers.java" 1526] [clojure.lang.Numbers multiply "Numbers.java" 1892] [clojure.lang.Numbers$LongOps multiply "Numbers.java" 472] [clojure.lang.Numbers multiply "Numbers.java" 148] [user$cuboid_volume invokeStatic "form-init4113422226954981451.clj" 1 [user$cuboid_volume invoke "form-init4113422226954981451.clj" 155] [clojure.lang.AFn applyToHelper "AFn.java" 154] [clojure.lang.AFn applyTo "AFn.java" 144] [clojure.core$apply invokeStatic "core.clj" 657] [clojure.core$apply invoke "core.clj" 652] [clojure.spec.test.alpha$check_call invokeStatic "alpha.clj" 292]

Slide 38

Slide 38 text

ここでは 乗算でinteger overflowが発⽣しうることが判明 :smallest [(#:user{:side-a 1, :side-b 23021144, :side-c 400647858198})] user> (cuboid-volume #::{:side-a 1 :side-b 23021144 :side-c 400647858198}) ArithmeticException integer overflow clojure.lang.Numbers.throw IntOverflow (Numbers.java:1526)

Slide 39

Slide 39 text

integer overflowしないように関数 * を *' に変更 user> (defn cuboid-volume [{::keys [side-a side-b side-c]}] (*' side-a side-b side-c)) #'user/cuboid-volume user> (cuboid-volume #::{:side-a 1 :side-b 23021144 :side-c 400647858198}) 9223372036867738512N

Slide 40

Slide 40 text

デフォルト1000回の試⾏で正常にテストをパス user> (stest/check `cuboid-volume) ({:spec #object[clojure.spec.alpha$fspec_impl$reify__1215 0x765a cd43 "clojure.spec.alpha$fspec_impl$reify__1215@765acd43"], :clo jure.spec.test.check/ret {:result true, :num-tests 1000, :seed 1 505803853835}, :sym user/cuboid-volume}) user> (stest/summarize-results *1) {:sym user/cuboid-volume} {:total 1, :check-passed 1}

Slide 41

Slide 41 text

最終結果 (ns spec-examples.geometry (:require [clojure.spec.alpha :as s])) ;; specs (s/def ::length (s/and number? pos?)) (s/def ::side-a ::length) (s/def ::side-b ::length) (s/def ::side-c ::length) (s/def ::cuboid (s/keys :req [::side-a ::side-b ::side-c]) (s/fdef cuboid-volume :args (s/cat :cuboid ::cuboid) :ret number?) ;; implementation (defn cuboid-volume [{::keys [side-a side-b side-c]}] (*' side-a side-b side-c))

Slide 42

Slide 42 text

述語(predicate)で仕様が書ける 値に対する制約が柔軟に表現できる Clojureの動的な性質と親和性が⾮常に⾼い コンパイル時ではなく実⾏時 REPL駆動開発とプロパティベーストテストで制 約を満たしていることを保証する戦略 漸進的型付け/静的⾔語化とは異なる未来 ⾃動プロパティベーストテストが便利

Slide 43

Slide 43 text

clojure.specを活⽤して 変更に強いClojureコードを書こう (*> ᴗ •*)ゞ

Slide 44

Slide 44 text

Vive les S-expressions ! Long live S-expressions!

Slide 45

Slide 45 text

Further Reading example code lagenorhynque/spec-examples clojure.spec vs core.typed vs schema

Slide 46

Slide 46 text

of cial site clojure.spec - Rationale and Overview spec Guide clojure/clojure at clojure-1.9.0-beta1 clojure/spec.alpha clojure.spec - Clojure v1.9 API documentation clojure/core.specs.alpha clojure/core.typed plumatic/schema

Slide 47

Slide 47 text

video book Spec-ulation Keynote - Rich Hickey "Agility & Robustness: Clojure spec" by Stuart Halloway clojure.spec - David Nolen Clojure spec Screencast Series Programming Clojure, Third Edition