第4章: Clojure Spec による仕様定義¶
はじめに¶
Clojure Spec は、データ構造と関数の仕様を定義するための強力なライブラリです。型システムとは異なり、Spec は実行時にデータを検証し、自動テストを生成し、ドキュメントとしても機能します。
本章では、Spec を使ってデータの仕様を定義し、関数の契約を表現し、自動テスト生成を活用する方法を学びます。
1. 基本的なスペック定義¶
スペックとは¶
スペックは、データが満たすべき条件を宣言的に定義します。
(require '[clojure.spec.alpha :as s])
;; シンプルなスペック
(s/def ::name (s/and string? #(< 0 (count %) 100)))
(s/def ::age (s/and int? #(<= 0 % 150)))
(s/def ::email (s/and string? #(re-matches #".+@.+\..+" %)))
;; 列挙型のスペック
(s/def ::membership #{:bronze :silver :gold :platinum})
(s/def ::status #{:active :inactive :suspended})
スペックの検証¶
;; valid? で検証
(s/valid? ::name "田中太郎") ;; => true
(s/valid? ::name "") ;; => false(空文字列)
(s/valid? ::age 25) ;; => true
(s/valid? ::age -1) ;; => false(負の数)
;; explain でエラー詳細を表示
(s/explain ::age -1)
;; val: -1 fails spec: :user/age predicate: (<= 0 % 150)
2. コレクションのスペック¶
ベクターとリスト¶
;; ベクターのスペック
(s/def ::tags (s/coll-of string? :kind vector? :min-count 0 :max-count 10))
(s/valid? ::tags ["tag1" "tag2"]) ;; => true
(s/valid? ::tags '("tag1")) ;; => false(リストは不可)
マップのスペック¶
;; 必須キーとオプションキー
(s/def ::person
(s/keys :req-un [::name ::age]
:opt-un [::email ::membership]))
(s/valid? ::person {:name "田中" :age 30}) ;; => true
(s/valid? ::person {:name "田中" :age 30 :email "tanaka@example.com"}) ;; => true
(s/valid? ::person {:name "田中"}) ;; => false(age が欠落)
入れ子のマップ¶
(s/def ::street string?)
(s/def ::city string?)
(s/def ::postal-code (s/and string? #(re-matches #"\d{3}-\d{4}" %)))
(s/def ::address
(s/keys :req-un [::street ::city ::postal-code]))
(s/def ::person-with-address
(s/keys :req-un [::name ::age ::address]))
(s/valid? ::person-with-address
{:name "田中"
:age 30
:address {:street "東京都渋谷区1-1-1"
:city "渋谷区"
:postal-code "150-0001"}})
;; => true
3. ドメインモデルの定義¶
商品と注文¶
;; 商品
(s/def ::product-id (s/and string? #(re-matches #"PROD-\d{5}" %)))
(s/def ::product-name (s/and string? #(< 0 (count %) 200)))
(s/def ::price (s/and number? pos?))
(s/def ::quantity (s/and int? pos?))
(s/def ::product
(s/keys :req-un [::product-id ::product-name ::price]
:opt-un [::description ::category]))
;; 注文アイテム
(s/def ::order-item
(s/keys :req-un [::product-id ::quantity ::price]))
;; 注文
(s/def ::order-id (s/and string? #(re-matches #"ORD-\d{8}" %)))
(s/def ::customer-id (s/and string? #(re-matches #"CUST-\d{6}" %)))
(s/def ::items (s/coll-of ::order-item :min-count 1))
(s/def ::order
(s/keys :req-un [::order-id ::customer-id ::items ::order-date]
:opt-un [::total ::status]))
4. 関数仕様の定義(fdef)¶
基本的な fdef¶
s/fdef を使って関数の引数、戻り値、および引数と戻り値の関係を定義します。
(defn calculate-item-total
"注文アイテムの合計を計算する"
[{:keys [price quantity]}]
(* price quantity))
(s/fdef calculate-item-total
:args (s/cat :item ::order-item)
:ret number?
:fn (s/and #(pos? (:ret %))
#(= (:ret %)
(* (get-in % [:args :item :price])
(get-in % [:args :item :quantity])))))
fdef の構成要素¶
- :args: 引数のスペック(
s/catで名前付き引数を定義) - :ret: 戻り値のスペック
- :fn: 引数と戻り値の関係を定義する述語
(defn apply-discount
"割引を適用する"
[total discount-rate]
(* total (- 1 discount-rate)))
(s/fdef apply-discount
:args (s/cat :total ::total :discount-rate (s/and number? #(<= 0 % 1)))
:ret number?
:fn #(<= (:ret %) (get-in % [:args :total]))) ;; 戻り値は元の値以下
5. 多引数と可変長引数¶
オーバーロードされた関数¶
(defn create-person
"人物を作成する"
([name age]
{:name name :age age})
([name age email]
{:name name :age age :email email}))
(s/fdef create-person
:args (s/alt :two-args (s/cat :name ::name :age ::age)
:three-args (s/cat :name ::name :age ::age :email ::email))
:ret ::person)
可変長引数¶
(defn sum-prices
"複数の価格を合計する"
[& prices]
(reduce + 0 prices))
(s/fdef sum-prices
:args (s/* ::price)
:ret number?)
6. 条件付きスペック(マルチスペック)¶
データの内容に応じて異なるスペックを適用する場合は、マルチスペックを使用します。
(s/def ::notification-type #{:email :sms :push})
(defmulti notification-spec :type)
(defmethod notification-spec :email [_]
(s/keys :req-un [::type ::to ::subject ::body]))
(defmethod notification-spec :sms [_]
(s/keys :req-un [::type ::phone-number ::body]))
(defmethod notification-spec :push [_]
(s/keys :req-un [::type ::device-token ::body]))
(s/def ::notification (s/multi-spec notification-spec :type))
;; 検証
(s/valid? ::notification
{:type :email
:to "test@example.com"
:subject "テスト"
:body "本文"})
;; => true
(s/valid? ::notification
{:type :sms
:phone-number "090-1234-5678"
:body "本文"})
;; => true
7. カスタムジェネレータ¶
スペックに対応するテストデータを自動生成するジェネレータを定義できます。
(require '[clojure.spec.gen.alpha :as gen])
(defn product-id-gen
"商品IDのジェネレータ"
[]
(gen/fmap #(str "PROD-" (format "%05d" %))
(gen/choose 0 99999)))
(s/def ::product-id-with-gen
(s/with-gen
(s/and string? #(re-matches #"PROD-\d{5}" %))
product-id-gen))
;; サンプル生成
(gen/sample (s/gen ::product-id-with-gen) 5)
;; => ("PROD-00042" "PROD-00001" "PROD-12345" "PROD-00000" "PROD-99999")
メールアドレスのジェネレータ¶
(defn email-gen
"メールアドレスのジェネレータ"
[]
(gen/fmap (fn [[user domain]]
(str user "@" domain ".com"))
(gen/tuple (gen/such-that #(not (empty? %))
(gen/string-alphanumeric))
(gen/such-that #(not (empty? %))
(gen/string-alphanumeric)))))
8. バリデーションとエラーハンドリング¶
バリデーション関数¶
(defn validate-person
"人物データを検証し、結果を返す"
[person]
(if (s/valid? ::person person)
{:valid true :data person}
{:valid false
:errors (s/explain-data ::person person)}))
(validate-person {:name "田中" :age 30})
;; => {:valid true, :data {:name "田中", :age 30}}
(validate-person {:name "" :age 30})
;; => {:valid false, :errors {...}}
conform による値の変換¶
(defn conform-or-throw
"スペックに適合しない場合は例外をスロー"
[spec data]
(let [conformed (s/conform spec data)]
(if (= conformed ::s/invalid)
(throw (ex-info "Validation failed"
{:spec spec
:data data
:problems (s/explain-data spec data)}))
conformed)))
9. インストルメンテーション¶
開発時に関数の引数を自動検証できます。
(require '[clojure.spec.test.alpha :as stest])
;; インストルメンテーションを有効化
(stest/instrument)
;; これ以降、fdef が定義された関数は引数が自動検証される
(calculate-item-total {:product-id "INVALID" :quantity -1 :price 0})
;; => ExceptionInfo: Call to #'user/calculate-item-total did not conform to spec
;; 無効化
(stest/unstrument)
10. テスト生成¶
check 関数で自動的にテストを生成・実行できます。
(stest/check `calculate-item-total)
;; 100個のランダムな入力でテストを実行
(stest/check `apply-discount {:clojure.spec.test.check/opts {:num-tests 1000}})
;; 1000個のテストケースで実行
まとめ¶
本章では、Clojure Spec について学びました:
- 基本的なスペック: 述語と組み合わせたデータ検証
- コレクションスペック: ベクター、マップの構造定義
- ドメインモデル: ビジネスデータの仕様定義
- fdef: 関数の契約(引数、戻り値、関係)
- マルチスペック: 条件付きの仕様定義
- カスタムジェネレータ: テストデータの自動生成
- バリデーション: 実行時検証とエラーハンドリング
- インストルメンテーション: 開発時の自動検証
- テスト生成: 自動プロパティベーステスト
Spec を活用することで、動的型付け言語の柔軟性を保ちながら、堅牢なデータ検証とドキュメント化を実現できます。
参考コード¶
本章のコード例は以下のファイルで確認できます:
- ソースコード:
apps/clojure/part2/src/clojure_spec.clj - テストコード:
apps/clojure/part2/spec/clojure_spec_spec.clj
次章予告¶
次章では、プロパティベーステストについて学びます。test.check を使った生成的テストの手法を探ります。