$30 off During Our Annual Pro Sale. View Details »

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. Everyday Life Everyday Life with clojure.spec with clojure.spec

  2. 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"])
  3. 1. Clojure 2. Clojure 開発で困ること 3. clojure.spec

  4. Clojure Clojure

  5. Clojure とは Clojure とは 関数型⾔語 JVM ⾔語 Lisp 動的型付き⾔語 が作った"simple"

    な⾔語 Rich Hickey
  6. 簡単なプログラムの例 簡単なプログラムの例 dev> (defn hello [name] (println (str "Hello, "

    name "!"))) #'dev/hello dev> (hello "World") Hello, World! nil dev> (hello "Clojure") Hello, Clojure! nil
  7. Clojure での開発のしかた Clojure での開発のしかた コンパイルが通るようにひとまとまりのコードを 書き、コンパイルできたらたいてい期待通りに動 作する 優れた型システムを備えた静的型付き⾔語の イメージ(?) 動くと思われるひとまとまりのコードを書き、動

    かしてみて期待通りでなければ適宜デバッグする 典型的な動的型付き⾔語のイメージ(?)
  8. REPL と繋がったエディタで⼩さな単位で動かし ながらコードを書き、書き上がったひとまとまり のコードは期待通りに動作する Clojure などLisp 系⾔語での開発スタイル いわゆる「REPL 駆動開発」 多くのLisp

    ではREPL 周りのツールが⾼ 度に発達している REPL と連携しながらの開発を前提に⾔ 語が設計されているとさえ考えられる
  9. Clojure 開発で困ること Clojure 開発で困ること

  10. 例えば、こんな関数を定義する 例えば、こんな関数を定義する 様々な暗黙の前提がある( 使う側は知る由もない) (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)))
  11. 使ってみると 使ってみると 正しい使い⽅を知っていれば期待通りに動作する 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!"}]
  12. しかし …… しかし ……

  13. 唐突に、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
  14. 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
  15. 明らかにDB 接続情報でないものを与えると SQLException が発⽣したり example> (find-artists "foo" {}) Execution error

    (SQLException) at java.sql.DriverManager/getConn ection (DriverManager.java:702). No suitable driver found for foo
  16. エラーメッセージが分かりづらい エラーメッセージの不親切さに定評がある fail-fast でない "garbage in, garbage out" ⼊出⼒として想定しているものが分からない ドキュメントで冗⻑かつ不明確に説明したい

    わけでもない 関数型⾔語なので不可解な副作⽤に悩まされ ることは少ないとはいえ……
  17. 従来のアプローチ 従来のアプローチ スキーマ記述とバリデーションのためのサー ドパーティライブラリ (→ ) gradual/optional typing のための準標準ライ ブラリ

    schema core.typed Typed Clojure
  18. 静的型付けの Clojure がほしい ? 静的型付けの Clojure がほしい ?

  19. clojure.spec clojure.spec

  20. コントラクト ( 契約 ) システム コントラクト ( 契約 ) システム

    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
  21. 標準ライブラリ clojure.spec 標準ライブラリ clojure.spec cf. 述語 (predicate) による仕様記述システム NOT 型システム

    spec.alpha spec-alpha2 (alpha.spec) core.specs.alpha
  22. clojure.spec を導⼊する clojure.spec を導⼊する example> (require '[clojure.spec.alpha :as s]) nil

  23. この関数に "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)))
  24. 仕様を⾃然⾔語で表現してみると 仕様を⾃然⾔語で表現してみると 引数 ds: javax.sql.DataSource オブジェクト {:keys [name ids sort-order]}:

    以下 のキーを含むかもしれない検索条件マップ :name: ⽂字列 :ids: ⾃然数の空でないシーケンス :sort-order: ソートキーのキーワー ドと昇順/ 降順の :asc または :desc の ペアの空でなく第1 要素についてユニー クなシーケンス
  25. 戻り値 アーティストマップのシーケンス アーティストマップ: 以下のキーを必ず 含むマップ :id: ⾃然数 :type: 1 (

    グループ) または 2 ( ソロ) :name: ⽂字列
  26. s/fdef s/fdef マクロで記述すると マクロで記述すると s/fdef は関数に対するspec を定義する ;;; 関数 find-artists

    に対する spec 定義のイメージ ;;; ,,, 部分を埋めたい (s/fdef find-artists :args (s/cat :ds ,,, ; 第 1 引数 :condition ,,,) ; 第 2 引数 :ret ,,,) ; 戻り値
  27. アーティストマップを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
  28. 戻り値のspec 定義が定まる ;;; 関数 find-artists に対する spec 定義のイメージ ;;; ,,,

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

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

    ,,, 部分を埋めたい (s/fdef find-artists :args (s/cat :ds #(instance? DataSource %) ; 第 1 引数 :condition ,,,) ; 第 2 引数 :ret (s/coll-of ::artist)) ; 戻り値
  31. :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
  32. :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
  33. 第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
  34. 関数の 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]
  35. 改めて使ってみると 改めて使ってみると 想定通りの⼊⼒に対して変わらず動作する 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!"}]
  36. そして …… そして ……

  37. 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
  38. 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
  39. 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]
  40. その他の主な活⽤⽅法 その他の主な活⽤⽅法 spec によるドキュメンテーション clojure.repl/doc の出⼒にも反映される spec によるバリデーション spec からサンプルデータの⾃動⽣成

    spec によるproperty-based testing cf. test.check
  41. 関連サードパーティライブラリ 関連サードパーティライブラリ : spec の instrument 時のチェックを 強化する : spec

    のエラーメッセージを⾒やすく表 ⽰する : 標準ライブラリ関数/ マクロに対す るspec を独⾃に提供する : spec を静的解析に利⽤する試み Orchestra Expound speculative spectrum
  42. clojure.spec の登場で clojure.spec の登場で Clojurian の⽇常は⼀変している Clojurian の⽇常は⼀変している イマドキのClojure 開発をぜひ体験しよう!

  43. Further Reading Further Reading Clojure Clojure Clojure/ClojureScript 関連リンク集 標準ライブラリ

  44. clojure.spec clojure.spec clojure.spec - Rationale and Overview ⽇本語版 spec Guide

  45. clojure.spec 関連ライブラリ clojure.spec 関連ライブラリ cf. Orchestra Expound Pinpointer speculative spectrum

  46. コントラクトシステム (Racket) コントラクトシステム (Racket) The Racket Guide > 7 Contracts

    The Racket Reference > 8 Contracts
  47. サンプルコード サンプルコード lagenorhynque/everyday-life-with-clojure-spec lagenorhynque/spec-examples