1K Views
April 17, 20
スライド概要
clojure.specの登場で変わったClojureプログラマの日常。
イマドキのClojure開発を体験しよう!
「楽しく楽にcoolにsmartに」を理想とするprogrammer/philosopher/liberalist/realist。 好きな言語はClojure, Haskell, Python, English, français, русский。 読書、プログラミング、語学、法学、数学が大好き! イルカと海も大好き🐬
Everyday Life with clojure.spec
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"] ["github.com/japan-clojurians/clojure-site-ja"]) :contributes
1. Clojure 開発で困ること 2. Clojure 3. clojure.spec
Clojure
とは Clojure 関数型⾔語 JVM⾔語 Lisp 動的型付き⾔語 Rich Hickeyが作った"simple"な⾔語
簡単なプログラムの例 dev> (defn hello [name] (println (str "Hello, " name "!"))) #'dev/hello dev> (hello "World") Hello, World! nil dev> (hello "Clojure") Hello, Clojure! nil
での開発のしかた Clojure コンパイルが通るようにひとまとまりのコードを 書き、コンパイルできたらたいてい期待通りに動 作する 優れた型システムを備えた静的型付き⾔語の イメージ(?) 動くと思われるひとまとまりのコードを書き、動 かしてみて期待通りでなければ適宜デバッグする 典型的な動的型付き⾔語のイメージ(?)
と繋がったエディタで⼩さな単位で動かし ながらコードを書き、書き上がったひとまとまり のコードは期待通りに動作する ClojureなどLisp系⾔語での開発スタイル いわゆる「REPL駆動開発」 多くのLispではREPL周りのツールが⾼ 度に発達している REPLと連携しながらの開発を前提に⾔ 語が設計されているとさえ考えられる REPL
開発で困ること Clojure
例えば、こんな関数を定義する (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))) 様々な暗黙の前提がある(使う側は知る由もない)
使ってみると
example> (find-artists
[#:artist{:id 1, :type
#:artist{:id 2, :type
#:artist{:id 3, :type
#:artist{:id 4, :type
#:artist{:id 5, :type
#:artist{:id 6, :type
(ds) {})
1, :name
1, :name
1, :name
1, :name
1, :name
1, :name
"Aqours"}
"CYaRon!"}
"AZALEA"}
"Guilty Kiss"}
"Saint Snow"}
"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!"}]
正しい使い⽅を知っていれば期待通りに動作する
しかし……
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
唐突に、LongからISeqを作る⽅法が分からない
と⾔われたり
Clojurianにはお馴染み😅
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
にアクセスするので⼊⼒の型が想定
と違うと
が発⽣したり
PostgreSQL
PSQLException
example> (find-artists "foo" {})
Execution error (SQLException) at java.sql.DriverManager/getConn
ection (DriverManager.java:702).
No suitable driver found for foo
明らかにDB接続情報でないものを与えると
SQLExceptionが発⽣したり
エラーメッセージが分かりづらい エラーメッセージの不親切さに定評がある😇 fail-fastでない "garbage in, garbage out" 🗑 ⼊出⼒として想定しているものが分からない ドキュメントで冗⻑かつ不明確に説明したい わけでもない 関数型⾔語なので不可解な副作⽤に悩まされ ることは少ないとはいえ……
従来のアプローチ schema スキーマ記述とバリデーションのためのサー ドパーティライブラリ core.typed (→ Typed Clojure) gradual/optional typing ブラリ のための準標準ライ
静的型付けのClojureがほしい?
clojure.spec
コントラクト(契約)システム の 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
標準ライブラリclojure.spec spec.alpha cf. spec-alpha2 (alpha.spec) core.specs.alpha 述語(predicate)による仕様記述システム NOT 型システム
を導⼊する clojure.spec example> (require '[clojure.spec.alpha :as s]) nil
この関数に"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)))
仕様を⾃然⾔語で表現してみると 引数 オブジェクト 以下 のキーを含むかもしれない検索条件マップ :name: ⽂字列 :ids: ⾃然数の空でないシーケンス :sort-order: ソートキーのキーワー ドと昇順/降順の :asc または :desc の ペアの空でなく第1要素についてユニー クなシーケンス ds: javax.sql.DataSource {:keys [name ids sort-order]}:
戻り値 アーティストマップのシーケンス アーティストマップ: 以下のキーを必ず 含むマップ :id: ⾃然数 :type: 1 (グループ) または 2 (ソロ) :name: ⽂字列
マクロで記述すると s/fdef 関数 ;;; find-artists ;;; ,,, 部分を埋めたい に対するspec定義のイメージ (s/fdef find-artists :args (s/cat :ds ,,, :condition ,,,) :ret ,,,) s/fdef ; ; ; 第1引数 第2引数 戻り値 は関数に対するspecを定義する
アーティストマップをspecとして記述してみる
example> (s/def
:artist/id
example> (s/def
:artist/type
example> (s/def
:artist/name
example> (s/def
:artist/id nat-int?)
:artist/type #{1 2})
:artist/name string?)
::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
は
述語)に名前を付ける
は を満たすかどうか判定する
s/def spec(=
s/valid? spec
関数 戻り値のspec定義が定まる ;;; find-artists ;;; ,,, 部分を埋めたい に対するspec定義のイメージ (s/fdef find-artists :args (s/cat :ds ,,, :condition ,,,) :ret (s/coll-of ::artist)) ; ; ; 第1引数 第2引数 戻り値
DataSource であることをspecとして記述してみる example> (import '(javax.sql DataSource)) javax.sql.DataSource example> (s/valid? #(instance? DataSource %) (ds)) true example> (s/valid? #(instance? DataSource %) "foo") false
関数 第1引数のspecが定まる ;;; find-artists ;;; ,,, 部分を埋めたい に対するspec定義のイメージ (s/fdef find-artists :args (s/cat :ds #(instance? DataSource %) :condition ,,,) :ret (s/coll-of ::artist)) ; ; ; 第1引数 第2引数 戻り値
: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
: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
第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
関数のspecを実装に組み込む (instrumentation) example> (require '[clojure.spec.test.alpha :as stest]) nil example> (stest/instrument `find-artists) [everyday-life-with-clojure-spec.example/find-artists] は関数のspecの引数に対す るチェックを関数の実装に組み込む 実際の開発環境では開発/テスト時に⾃動的 に組み込まれるように設定することが多い stest/instrument
改めて使ってみると
example> (find-artists
[#:artist{:id 1, :type
#:artist{:id 2, :type
#:artist{:id 3, :type
#:artist{:id 4, :type
#:artist{:id 5, :type
#:artist{:id 6, :type
(ds) {})
1, :name
1, :name
1, :name
1, :name
1, :name
1, :name
"Aqours"}
"CYaRon!"}
"AZALEA"}
"Guilty Kiss"}
"Saint Snow"}
"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!"}]
想定通りの⼊⼒に対して変わらず動作する
そして……
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 に違反すると直ちにエラーになってくれる ⼊⼒のどの値がどの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
:753).
"2" - failed: nat-int? at: [:condition :ids] spec: :artist/id
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]
その他の主な活⽤⽅法 によるドキュメンテーション clojure.repl/doc の出⼒にも反映される specによるバリデーション specからサンプルデータの⾃動⽣成 specによるproperty-based testing spec cf. test.check
関連サードパーティライブラリ の instrument 時のチェックを Orchestra: spec 強化する のエラーメッセージを⾒やすく表 Expound: spec ⽰する 標準ライブラリ関数/マクロに対す る を独⾃に提供する spectrum: specを静的解析に利⽤する試み speculative: spec
の登場で Clojurianの⽇常は⼀変している clojure.spec イマドキのClojure開発をぜひ体験しよう!
Further Reading Clojure Clojure/ClojureScript 標準ライブラリ 関連リンク集
clojure.spec clojure.spec - Rationale and Overview ⽇本語版 spec Guide
関連ライブラリ clojure.spec Orchestra Expound cf. Pinpointer speculative spectrum
コントラクトシステム(Racket) The Racket Guide > 7 Contracts The Racket Reference > 8 Contracts
サンプルコード lagenorhynque/everyday-life-with-clojure-spec lagenorhynque/spec-examples