📝Clojure spec

📝Clojure spec

up: 📂Clojure Core Languages

clojure.specとは #

Clojureにおいて 契約プログラミング を実施するためのライブラリ.

Clojure 1.9から導入された. これは 2017.12 の話なので古い書籍だとそもそもclojure.specの話題を扱っていない.

clojure.spec Usages #

Basics #

  • s/def
    • 満たすべき条件を宣言
  • s/valid?
    • 条件を検証
  • s/coll-of
    • 条件を満たす集合を宣言
  • s/keys
    • 条件を満たす名前付きの値の集合(ie. Map)の条件を定義
    • :reqは必須のキーワード, :optは任意のキーワード.
  • s/explain
    • 検証が失敗した理由を出力.
  • s/conform
    • 条件を満たす場合のみ与えられた値を検証を通過したキーワードに束縛.
  • s/cat
    • 束縛されるキーワードとその条件をまとめるという宣言.

以下の記事を参考.

What Clojure spec is and what you can do with it - Pixelated Noise Blog

(require '[clojure.spec.alpha :as s])

;; basic
;; s/defで満たすべき条件を宣言
;; s/valid? で検証
(s/def ::username string?)
(s/valid? ::username "foo")
(s/valid? #(> % 5) 10)

;; collections
;; s/coll-ofで条件を満たす集合を宣言
(s/def ::usernames (s/coll-of ::username))
(s/valid? ::usernames ["foo" "bar" "baz"])

;; Maps
;; s/keysで条件を満たす名前付きの値の集合(ie. Map)の条件を定義
;; :reqは必須のキーワード, :optは任意のキーワード.
(s/def ::password string?)
(s/def ::last-login number?)
(s/def ::comment string?)
(s/def ::user
  (s/keys
   :req [::username ::password]
   :opt [::comment ::last-login]))
(s/valid?
 ::user
 {::username   "rich"
  ::password   "zegure"
  ::comment    "this is a user"
  ::last-login 11000})

;; Explain
;; s/explainで検証が失敗した理由を出力.
(s/explain
 ::user
 {::username "rich"
  ::comment  "this is a user"})

s/def: ルールを定義する #

s/defで指定するkeywordは ::hogeか :foo/barである必要がある.

k must be namespaced keyword or resolvable symbol (c/and (ident? k) (namespace k))

言い換えると :fooのようなnamespaceから始まるslashを伴わない場合はエラーする.

clojure.spec conform #

いわば正規表現のような, 分配束縛のような機能を提供する.

Clojure specの強力な機能のひとつ.

  • s/conform
    • 条件を満たす場合のみ与えられた値を検証を通過したキーワードに束縛.
  • s/cat
    • 束縛されるキーワードとその条件をまとめるという宣言.
(s/def ::ingredient (s/cat :quantity number? :unit keyword?))
(s/conform ::ingredient [2 :teaspoon])
;; => {:quantity 2, :unit :teaspoon}

s/defからMapのルールを構築する #

s/defで定義したルールを組み合わせてMapやRecordのルールを構築することができる. 結局Clojureの世界ではassociated dataの受け渡しであらゆることを処理していく世界観なのでMapのチェックは大事.

s/def と s/keys :req-un/:opt-un をつかって構築する.

(s/def ::person (s/keys :req-un [::first-name ::last-name]
                        :opt-un [:email]))

s/conform を利用してチェックと生成を行う.

;; Map
(s/conform ::person {:first-name "Foo" :last-name "bar"})
;; Record
(s/conform ::person (map->Person
                    {:first-name "Foo" :last-name "bar"}))

qualified keywords vs unqualified keywords #

なおここで qualified keywordsとunqualified keywordsという概念が登場する. qualified keywordsはnamespaceに属するキーワーで :foo/barのような表記. unqualified keywordsは :fooの表記.

s/keys で構築するとき :req/:req-unを選択できるが:reqならqualified keywordを指定する必要がある. たとえばintegrantはqualified keywordをつかって構成を定義している.

しかし大抵はmapといえばunqualifiedなので :req-unでいいだろう.

clojure.spec: s/fdef #

関数というものが[:defn :name :doc :args :body]の5つのキーに束縛されたコードの集合とみなせばそれらに対する検証をすることで関数が検証できる, という考え方.

(s/def ::function (s/cat :defn #{'defn}
                         :name symbol?
                         :doc (s/? string?)
                         :args vector?
                         :body (s/+ list?)))

あるnamespace foo で s/def で ::foo-x と定義したものを別のnamespaceから使う場合は ::foo/foo-x となる.

(ns foo.core
  (:requre [clojure.spec.alpha :as s]))
(s/def ::foo-x pos?)

(ns bar.core
  (:requre [clojure.spec.alpha :as s]
           [foo.core :as foo]))
(s/valid? ::foo/foo-x 1)

clojure.spec Instrument #

clojure.spec.test.alpha/instrumentという関数を実行するとその後にspecを定義してある関数を実行するたびにspec通りの引数や戻り値になっているかをチェックしてくれる.

ref: Instrument - spec Guide

clojure.specによる防衛的プログラミング #

ref: 📝Clojure Architecture

  • 防衛的プログラミング(Secure Programming)
  • コントラクトシステム(Contract System)

簡単に引数チェックをするらば, ClojureのSpecial Formの(:pre, :post)の利用もできる.

ref. Clojure - Special Forms

(defn constrained-sqr [x]
    {:pre  [(pos? x)]
     :post [(> % 16), (< % 225)]}
    (* x x))

もしくはpredicatesの部分だけclojure.specをつかう.

(s/def ::x pos?)
(defn constrained-sqr2 [x]
  {:pre [(s/valid? ::x x)]}
  (* x x))

s/assert で中身をチェックした結果をletでbindしてもいい. このとき s/conform を利用すると例外が上がらずに :clojure.spec.alpha.invalid がbindingされて処理が継続するので注意.

詳しくはこちら -> Using spec for validateion - clojure.org

clojure.orgにもあるように気軽な引数チェックならpreでもいいが, ガチりたい場合はsdefを導入すること.

💡clojure.specは Schemaではない #

Eric Normand さんの以下の記事より.

5 Differences between clojure.spec and Schema

clojure.spceもSchemaも同じ課題を解決しようとした点で似ている. しかし両者は本質的に別のものである.

  • clojure.specは “Data DSL"ではない.
  • clojure.specは namespaced keywrodsを好む.
  • clojure.specは強力なシーケンス検証機能がある.
  • clojure.specは検証(checking)とパース(parsing)を兼ね備える(conform).
  • clojure.specはtest.checkとの強い連携がある.

clojure.specと型ヒントとの比較 #

ref: 📝Clojure: 型ヒント(Type Hinting)

References #