(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"])
-
Clojure
-
Clojure開発で困ること
-
clojure.spec
-
関数型言語
-
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
-
コンパイルが通るようにひとまとまりのコードを書き、コンパイルできたらたいてい期待通りに動作する- 優れた型システムを備えた静的型付き言語のイメージ(?)
-
動くと思われるひとまとまりのコードを書き、動かしてみて期待通りでなければ適宜デバッグする- 典型的な動的型付き言語のイメージ(?)
- REPLと繋がったエディタで小さな単位で動かしながらコードを書き、書き上がったひとまとまりのコードは期待通りに動作する
-
ClojureなどLisp系言語での開発スタイル
-
いわゆる「REPL駆動開発」
-
多くのLispではREPL周りのツールが高度に発達している
-
REPLと連携しながらの開発を前提に言語が設計されているとさえ考えられる
-
-
(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!"}]
- 正しい使い方を知っていれば期待通りに動作する
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" 🗑
-
入出力として想定しているものが分からない
- ドキュメントで冗長かつ不明確に説明したいわけでもない
- 関数型言語なので不可解な副作用に悩まされることは少ないとはいえ……
-
- スキーマ記述とバリデーションのためのサードパーティライブラリ
-
- gradual/optional typingのための準標準ライブラリ
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
述語(predicate)による仕様記述システム
NOT 型システム
example> (require '[clojure.spec.alpha :as s])
nil
(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
: 文字列
- アーティストマップ: 以下のキーを必ず含むマップ
- アーティストマップのシーケンス
;;; 関数 find-artists に対するspec定義のイメージ
;;; ,,, 部分を埋めたい
(s/fdef find-artists
:args (s/cat :ds ,,, ; 第1引数
:condition ,,,) ; 第2引数
:ret ,,,) ; 戻り値
s/fdef
は関数に対するspecを定義する
アーティストマップを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
s/def
はspec(= 述語)に名前を付けるs/valid?
はspecを満たすかどうか判定する
戻り値の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
example> (require '[clojure.spec.test.alpha :as stest])
nil
example> (stest/instrument `find-artists)
[everyday-life-with-clojure-spec.example/find-artists]
stest/instrument
は関数のspecの引数に対するチェックを関数の実装に組み込む- 実際の開発環境では開発/テスト時に自動的に組み込まれるように設定することが多い
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!"}]
- 想定通りの入力に対して変わらず動作する
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]
-
specによるドキュメンテーション
clojure.repl/doc
の出力にも反映される
-
specによるバリデーション
-
specからサンプルデータの自動生成
-
specによるproperty-based testing
- cf. test.check
-
Orchestra: specの
instrument
時のチェックを強化する -
Expound: specのエラーメッセージを見やすく表示する
-
speculative: 標準ライブラリ関数/マクロに対するspecを独自に提供する
-
spectrum: specを静的解析に利用する試み
イマドキのClojure開発をぜひ体験しよう!