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

Everyday Life with clojure.spec

Everyday Life with clojure.spec

clojure.specの登場で変わったClojureプログラマの日常。
イマドキのClojure開発を体験しよう!

Kent OHASHI

April 17, 2020
Tweet

More Decks by Kent OHASHI

Other Decks in Programming

Transcript

  1. lagénorhynque lagénorhynque (defprofile lagénorhynque :id @lagenorhynque :reading "/laʒenɔʁɛ̃ k/" :aliases

    [" カマイルカ "] :languages [Clojure Haskell English français] :interests [programming language-learning law mathematics] :commits ["github.com/lagenorhynque/duct.module.pedestal" "github.com/lagenorhynque/duct.module.cambium"] :contributes ["github.com/japan-clojurians/clojure-site-ja"])
  2. 簡単なプログラムの例 簡単なプログラムの例 dev> (defn hello [name] (println (str "Hello, "

    name "!"))) #'dev/hello dev> (hello "World") Hello, World! nil dev> (hello "Clojure") Hello, Clojure! nil
  3. 例えば、こんな関数を定義する 例えば、こんな関数を定義する 様々な暗黙の前提がある( 使う側は知る由もない) (defn find-artists [ds {:keys [name ids

    sort-order]}] (jdbc/execute! ds (cond-> (sql/build :select :* :from :artist) name (merge-where [:like :name (str \% name \%)]) (seq ids) (merge-where [:in :id ids]) (seq sort-order) (#(apply merge-order-by % sort-order)) (empty? sort-order) (merge-order-by [:id :asc]) true sql/format)))
  4. 使ってみると 使ってみると 正しい使い⽅を知っていれば期待通りに動作する example> (find-artists (ds) {}) [#:artist{:id 1, :type

    1, :name "Aqours"} #:artist{:id 2, :type 1, :name "CYaRon!"} #:artist{:id 3, :type 1, :name "AZALEA"} #:artist{:id 4, :type 1, :name "Guilty Kiss"} #:artist{:id 5, :type 1, :name "Saint Snow"} #:artist{:id 6, :type 1, :name "Saint Aqours Snow"}] example> (find-artists (ds) {:name "Aq"}) [#:artist{:id 1, :type 1, :name "Aqours"} #:artist{:id 6, :type 1, :name "Saint Aqours Snow"}] example> (find-artists (ds) {:ids [2]}) [#:artist{:id 2, :type 1, :name "CYaRon!"}]
  5. 唐突に、Long からISeq を作る⽅法が分からない と⾔われたり Clojurian にはお馴染み example> (find-artists (ds) {:ids

    2}) Execution error (IllegalArgumentException) at everyday-life-with -clojure-spec.example/find-artists (example.clj:40). Don't know how to create ISeq from: java.lang.Long
  6. PostgreSQL にアクセスするので⼊⼒の型が想定 と違うとPSQLException が発⽣したり example> (find-artists (ds) {:ids ["2"]}) Execution

    error (PSQLException) at org.postgresql.core.v3.QueryE xecutorImpl/receiveErrorResponse (QueryExecutorImpl.java:2533). ERROR: operator does not exist: bigint = character varying Hint: No operator matches the given name and argument types. Y ou might need to add explicit type casts. Position: 32
  7. 明らかにDB 接続情報でないものを与えると SQLException が発⽣したり example> (find-artists "foo" {}) Execution error

    (SQLException) at java.sql.DriverManager/getConn ection (DriverManager.java:702). No suitable driver found for foo
  8. コントラクト ( 契約 ) システム コントラクト ( 契約 ) システム

    e.g. Racket のcontract system > (define/contract (maybe-invert i b) (-> integer? boolean? integer?) (if b (- i) i)) > (maybe-invert 1 #t) -1 > (maybe-invert #f 1) maybe-invert: contract violation expected: integer? given: #f in: the 1st argument of (-> integer? boolean? integer?) contract from: (function maybe-invert) blaming: top-level (assuming the contract is correct) at: eval:2.0 The Racket Reference > 8.2 Function Contracts
  9. この関数に "spec" を付けたい この関数に "spec" を付けたい (defn find-artists [ds {:keys

    [name ids sort-order]}] (jdbc/execute! ds (cond-> (sql/build :select :* :from :artist) name (merge-where [:like :name (str \% name \%)]) (seq ids) (merge-where [:in :id ids]) (seq sort-order) (#(apply merge-order-by % sort-order)) (empty? sort-order) (merge-order-by [:id :asc]) true sql/format)))
  10. 仕様を⾃然⾔語で表現してみると 仕様を⾃然⾔語で表現してみると 引数 ds: javax.sql.DataSource オブジェクト {:keys [name ids sort-order]}:

    以下 のキーを含むかもしれない検索条件マップ :name: ⽂字列 :ids: ⾃然数の空でないシーケンス :sort-order: ソートキーのキーワー ドと昇順/ 降順の :asc または :desc の ペアの空でなく第1 要素についてユニー クなシーケンス
  11. s/fdef s/fdef マクロで記述すると マクロで記述すると s/fdef は関数に対するspec を定義する ;;; 関数 find-artists

    に対する spec 定義のイメージ ;;; ,,, 部分を埋めたい (s/fdef find-artists :args (s/cat :ds ,,, ; 第 1 引数 :condition ,,,) ; 第 2 引数 :ret ,,,) ; 戻り値
  12. アーティストマップをspec として記述してみる s/def はspec(= 述語) に名前を付ける s/valid? はspec を満たすかどうか判定する example>

    (s/def :artist/id nat-int?) :artist/id example> (s/def :artist/type #{1 2}) :artist/type example> (s/def :artist/name string?) :artist/name example> (s/def ::artist (s/keys :req [:artist/id :artist/type :artist/name])) :everyday-life-with-clojure-spec.example/artist example> (s/valid? ::artist #:artist{:id 1 :type 2 :name "You Watanabe"}) true
  13. 戻り値のspec 定義が定まる ;;; 関数 find-artists に対する spec 定義のイメージ ;;; ,,,

    部分を埋めたい (s/fdef find-artists :args (s/cat :ds ,,, ; 第 1 引数 :condition ,,,) ; 第 2 引数 :ret (s/coll-of ::artist)) ; 戻り値
  14. DataSource であることをspec として記述してみる example> (import '(javax.sql DataSource)) javax.sql.DataSource example> (s/valid?

    #(instance? DataSource %) (ds)) true example> (s/valid? #(instance? DataSource %) "foo") false
  15. 第1 引数のspec が定まる ;;; 関数 find-artists に対する spec 定義のイメージ ;;;

    ,,, 部分を埋めたい (s/fdef find-artists :args (s/cat :ds #(instance? DataSource %) ; 第 1 引数 :condition ,,,) ; 第 2 引数 :ret (s/coll-of ::artist)) ; 戻り値
  16. :ids キーの値をspec として記述してみる example> (s/def ::ids (s/coll-of :artist/id :min-count 1))

    :everyday-life-with-clojure-spec.example/ids example> (s/valid? ::ids []) false example> (s/valid? ::ids [2]) true example> (s/valid? ::ids [2 4]) true example> (s/valid? ::ids [2 2]) true
  17. :sort-order キーの値をspec として記述してみる example> (s/def ::sort-order (s/and (s/coll-of (s/tuple #{:id

    :type :name} #{:asc :desc}) :min-count 1) #(apply distinct? (map first %)))) :everyday-life-with-clojure-spec.example/sort-order example> (s/valid? ::sort-order []) false example> (s/valid? ::sort-order [[:name :asc] [:id :desc]]) true example> (s/valid? ::sort-order [[:name :misc] [:id :desc]]) false example> (s/valid? ::sort-order [[:name :asc] [:name :desc]]) false
  18. 第2 引数のspec が定まり、関数のspec が仕上がる ex> (s/fdef find-artists :args (s/cat :ds

    #(instance? DataSource %) :condition (s/keys :opt-un [:artist/name ::ids ::sort-order])) :ret (s/coll-of ::artist)) everyday-life-with-clojure-spec.example/find-artists
  19. 関数の spec を実装に組み込む 関数の spec を実装に組み込む (instrumentation) (instrumentation) stest/instrument は関数のspec

    の引数に対す るチェックを関数の実装に組み込む 実際の開発環境では開発/ テスト時に⾃動的 に組み込まれるように設定することが多い example> (require '[clojure.spec.test.alpha :as stest]) nil example> (stest/instrument `find-artists) [everyday-life-with-clojure-spec.example/find-artists]
  20. 改めて使ってみると 改めて使ってみると 想定通りの⼊⼒に対して変わらず動作する example> (find-artists (ds) {}) [#:artist{:id 1, :type

    1, :name "Aqours"} #:artist{:id 2, :type 1, :name "CYaRon!"} #:artist{:id 3, :type 1, :name "AZALEA"} #:artist{:id 4, :type 1, :name "Guilty Kiss"} #:artist{:id 5, :type 1, :name "Saint Snow"} #:artist{:id 6, :type 1, :name "Saint Aqours Snow"}] example> (find-artists (ds) {:name "Aq"}) [#:artist{:id 1, :type 1, :name "Aqours"} #:artist{:id 6, :type 1, :name "Saint Aqours Snow"}] example> (find-artists (ds) {:ids [2]}) [#:artist{:id 2, :type 1, :name "CYaRon!"}]
  21. spec に違反すると直ちにエラーになってくれる ⼊⼒のどの値がどのspec に違反しているか教え てくれる example> (find-artists (ds) {:ids 2})

    Execution error - invalid arguments to everyday-life-with-clojur e-spec.example/find-artists at (form-init8369102478102661347.clj :747). 2 - failed: coll? at: [:condition :ids] spec: :everyday-life-wit h-clojure-spec.example/ids
  22. example> (find-artists (ds) {:ids ["2"]}) Execution error - invalid arguments

    to everyday-life-with-clojur e-spec.example/find-artists at (form-init8369102478102661347.clj :753). "2" - failed: nat-int? at: [:condition :ids] spec: :artist/id
  23. example> (find-artists "foo" {}) Execution error - invalid arguments to

    everyday-life-with-clojur e-spec.example/find-artists at (form-init8369102478102661347.clj :750). "foo" - failed: (instance? javax.sql.DataSource %) at: [:ds]
  24. 関連サードパーティライブラリ 関連サードパーティライブラリ : spec の instrument 時のチェックを 強化する : spec

    のエラーメッセージを⾒やすく表 ⽰する : 標準ライブラリ関数/ マクロに対す るspec を独⾃に提供する : spec を静的解析に利⽤する試み Orchestra Expound speculative spectrum