Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Spectacular Future with clojure.spec

Kent OHASHI
September 20, 2017

Spectacular Future with clojure.spec

clojure.specを活用して変更に強いClojureコードを書こう(*> ᴗ •*)ゞ
cf. https://github.com/lagenorhynque/spec-examples

Kent OHASHI

September 20, 2017
Tweet

More Decks by Kent OHASHI

Other Decks in Programming

Transcript

  1. 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])
  2. リテラル 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
  3. 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))
  4. 例: 直⽅体の体積計算 直⽅体の体積 = 辺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
  5. エラー 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)
  6. 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))
  7. 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))
  8. 「⻑さ」をspecで表現 でキーワード :user/length に述語 number? を登録 user> (s/def ::length number?)

    :user/length user> (doc ::length) ------------------------- :user/length Spec number? nil s/def
  9. ⼀致しない原因を調べる で⼀致しない詳細原因を確認 ここでは 値 "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
  10. 「⻑さ」に制約を加える 論理演算⼦で制約を追加 は論理積 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
  11. 「⻑さ」で辺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
  12. 辺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
  13. 直⽅体に⼀致する値を調べる 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
  14. ⼀致しない原因を調べる :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
  15. 値 #: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
  16. 値 #: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
  17. 直⽅体のサンプルを⽣成 複合的なデータのサンプルも⽣成できる 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})
  18. 直⽅体の体積計算の仕様を表現 で関数 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
  19. 引数に対するチェックを有効化 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]
  20. パス [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)
  21. 関数の動作確認 で引数の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
  22. ⾃動プロパティベーストテスト 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]
  23. ここでは 乗算で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)
  24. 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
  25. デフォルト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}
  26. 最終結果 (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))
  27. 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
  28. 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