Everyday LifeEveryday Life
with clojure.specwith clojure.spec
lagénorhynquelagé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"])
1. Clojure
2. Clojure開発で困ること
3. clojure.spec
ClojureClojure
ClojureとはClojureとは
関数型⾔語
JVM⾔語
Lisp
動的型付き⾔語
が作った"simple"な⾔語Rich Hickey
簡単なプログラムの例簡単なプログラムの例dev> (defn hello [name]
(println (str "Hello, " name "!")))
#'dev/hello
dev> (hello "World")
Hello, World!
nil
dev> (hello "Clojure")
Hello, Clojure!
nil
Clojureでの開発のしかたClojureでの開発のしかた
コンパイルが通るようにひとまとまりのコードを
書き、コンパイルできたらたいてい期待通りに動
作する
優れた型システムを備えた静的型付き⾔語の
イメージ(?)
動くと思われるひとまとまりのコードを書き、動
かしてみて期待通りでなければ適宜デバッグする
典型的な動的型付き⾔語のイメージ(?)
REPLと繋がったエディタで⼩さな単位で動かし
ながらコードを書き、書き上がったひとまとまり
のコードは期待通りに動作する
ClojureなどLisp系⾔語での開発スタイル
いわゆる「REPL駆動開発」
多くのLispではREPL周りのツールが⾼
度に発達している
REPLと連携しながらの開発を前提に⾔
語が設計されているとさえ考えられる
Clojure開発で困ること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 (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!"}]
しかし……しかし……
唐突に、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
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
明らかにDB接続情報でないものを与えると
SQLExceptionが発⽣したり
example> (find-artists "foo" {})
Execution error (SQLException) at java.sql.DriverManager/getConn
ection (DriverManager.java:702).
No suitable driver found for foo
エラーメッセージが分かりづらい
エラーメッセージの不親切さに定評がある😇
fail-fastでない
"garbage in, garbage out" 🗑
⼊出⼒として想定しているものが分からない
ドキュメントで冗⻑かつ不明確に説明したい
わけでもない
関数型⾔語なので不可解な副作⽤に悩まされ
ることは少ないとはいえ……
従来のアプローチ従来のアプローチ
スキーマ記述とバリデーションのためのサー
ドパーティライブラリ
(→ )
gradual/optional typingのための準標準ライ
ブラリ
schema
core.typed Typed Clojure
静的型付けのClojureがほしい?静的型付けのClojureがほしい?
clojure.specclojure.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標準ライブラリclojure.spec
cf.
述語(predicate)による仕様記述システム
NOT 型システム
spec.alpha
spec-alpha2 (alpha.spec)
core.specs.alpha
clojure.specを導⼊するclojure.specを導⼊するexample> (require '[clojure.spec.alpha :as s])
nil
この関数に"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)))
仕様を⾃然⾔語で表現してみると仕様を⾃然⾔語で表現してみると
引数
ds: javax.sql.DataSource オブジェクト
{:keys [name ids sort-order]}: 以下
のキーを含むかもしれない検索条件マップ
:name: ⽂字列
:ids: ⾃然数の空でないシーケンス
:sort-order: ソートキーのキーワー
ドと昇順/降順の:asc または:desc の
ペアの空でなく第1要素についてユニー
クなシーケンス
戻り値
アーティストマップのシーケンス
アーティストマップ: 以下のキーを必ず
含むマップ
:id: ⾃然数
:type: 1 (グループ) または2 (ソロ)
:name: ⽂字列
s/fdefs/fdef マクロで記述するとマクロで記述すると
s/fdef は関数に対するspecを定義する
;;; 関数 find-artists に対するspec定義のイメージ
;;; ,,, 部分を埋めたい
(s/fdef find-artists
:args (s/cat :ds ,,, ; 第1引数
:condition ,,,) ; 第2引数
:ret ,,,) ; 戻り値
アーティストマップを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
戻り値のspec定義が定まる
;;; 関数 find-artists に対するspec定義のイメージ
;;; ,,, 部分を埋めたい
(s/fdef find-artists
:args (s/cat :ds ,,, ; 第1引数
:condition ,,,) ; 第2引数
:ret (s/coll-of ::artist)) ; 戻り値
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 %) ; 第1引数
:condition ,,,) ; 第2引数
:ret (s/coll-of ::artist)) ; 戻り値
: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を実装に組み込む関数の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]
改めて使ってみると改めて使ってみると
想定通りの⼊⼒に対して変わらず動作する
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!"}]
そして……そして……
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
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]
その他の主な活⽤⽅法その他の主な活⽤⽅法
specによるドキュメンテーション
clojure.repl/doc の出⼒にも反映される
specによるバリデーション
specからサンプルデータの⾃動⽣成
specによるproperty-based testing
cf. test.check
関連サードパーティライブラリ関連サードパーティライブラリ
: specのinstrument 時のチェックを
強化する
: specのエラーメッセージを⾒やすく表
⽰する
: 標準ライブラリ関数/マクロに対す
るspecを独⾃に提供する
: specを静的解析に利⽤する試み
Orchestra
Expound
speculative
spectrum
clojure.specの登場でclojure.specの登場で
Clojurianの⽇常は⼀変しているClojurianの⽇常は⼀変している
イマドキのClojure開発をぜひ体験しよう!
Further ReadingFurther Reading
ClojureClojure
Clojure/ClojureScript関連リンク集
標準ライブラリ
clojure.specclojure.spec
clojure.spec - Rationale and Overview
⽇本語版
spec Guide
clojure.spec関連ライブラリclojure.spec関連ライブラリ
cf.
Orchestra
Expound
Pinpointer
speculative
spectrum
コントラクトシステム(Racket)コントラクトシステム(Racket)
The Racket Guide > 7 Contracts
The Racket Reference > 8 Contracts
サンプルコードサンプルコード
lagenorhynque/everyday-life-with-clojure-spec
lagenorhynque/spec-examples

Everyday Life with clojure.spec