Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Spectacular Future with clojure.spec

1,262 views

Published on

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

Published in: Software

Spectacular Future with clojure.spec

  1. 1. Spectacular Future with clojure.spec
  2. 2. 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])
  3. 3. 「Clojureをプロダクトに導⼊した話」
  4. 4. Clojure
  5. 5. Contents 1. Clojure Quick Intro 2. New Feature: clojure.spec
  6. 6. Clojure Quick Intro
  7. 7. Clojure Lisp S式, マクロ, etc. REPL駆動開発 関数型プログラミング⾔語 動的⾔語 JVM⾔語 (cf. ClojureScript) ⇒ シンプルで強⼒な⾔語
  8. 8. リテラル 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
  9. 9. 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))
  10. 10. シンタックス オペレータ 関数 マクロ 特殊形式 (op arg1 arg2 ... argn)
  11. 11. New Feature: clojure.spec
  12. 12. 例: 直⽅体の体積計算 直⽅体の体積 = 辺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
  13. 13. エラー 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)
  14. 14. 問題点 動的⾔語なのでコンパイル時に引数の型の不整合が 検出されない 少なくとも実⾏時に分かりやすいエラーになって ほしい マップのkey-valueに対するチェックがない 動的なデータを柔軟に表現したい
  15. 15. 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))
  16. 16. 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))
  17. 17. clojure.spec 述語(predicate)を組み合わせて仕様(spec)を書いて ドキュメント バリデーション 詳細なエラー報告 パースと分配束縛 データ⽣成 プロパティベーストテスト などを実現する仕組み
  18. 18. 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
  19. 19. 「⻑さ」をspecで表現 でキーワード :user/length に述語 number? を登録 user> (s/def ::length number?) :user/length user> (doc ::length) ------------------------- :user/length Spec number? nil s/def
  20. 20. 「⻑さ」に⼀致する値を調べる で ::length のspecに具体的な値が ⼀致するか確認 ⼀致しなければ ::s/invalid user> (s/conform ::length 3) 3 user> (s/conform ::length "3") :clojure.spec.alpha/invalid s/conform
  21. 21. ⼀致しない原因を調べる で⼀致しない詳細原因を確認 ここでは 値 "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
  22. 22. 「⻑さ」のサンプルを⽣成 で 互換なジェネレータを取得 でサンプルを⽣成 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
  23. 23. 「⻑さ」に制約を加える 論理演算⼦で制約を追加 は論理積 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
  24. 24. 「⻑さ」で辺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
  25. 25. 辺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
  26. 26. 直⽅体に⼀致する値を調べる 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
  27. 27. ⼀致しない原因を調べる :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
  28. 28. 値 #: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
  29. 29. 値 #: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
  30. 30. 直⽅体のサンプルを⽣成 複合的なデータのサンプルも⽣成できる 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})
  31. 31. 直⽅体の体積計算の仕様を表現 で関数 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
  32. 32. 仕様を満たすように関数を実装 user> (defn cuboid-volume [{::keys [side-a side-b side-c]}] (* side-a side-b side-c)) #'user/cuboid-volume
  33. 33. 引数に対するチェックを有効化 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]
  34. 34. 引数のspecを満たす値に適⽤すると期待した計算 結果が得られる user> (cuboid-volume #::{:side-a 1 :side-b 2 :side-c 3}) 6
  35. 35. パス [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)
  36. 36. 関数の動作確認 で引数の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
  37. 37. ⾃動プロパティベーストテスト 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]
  38. 38. ここでは 乗算で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)
  39. 39. 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
  40. 40. デフォルト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}
  41. 41. 最終結果 (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))
  42. 42. 述語(predicate)で仕様が書ける 値に対する制約が柔軟に表現できる Clojureの動的な性質と親和性が⾮常に⾼い コンパイル時ではなく実⾏時 REPL駆動開発とプロパティベーストテストで制 約を満たしていることを保証する戦略 漸進的型付け/静的⾔語化とは異なる未来 ⾃動プロパティベーストテストが便利
  43. 43. clojure.specを活⽤して 変更に強いClojureコードを書こう (*> ᴗ •*)ゞ
  44. 44. Vive les S-expressions ! Long live S-expressions!
  45. 45. Further Reading example code lagenorhynque/spec-examples clojure.spec vs core.typed vs schema
  46. 46. 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
  47. 47. 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

×