Clojureは動的言語なので、型の情報なしで開発を進めていくのが基本です。
動的な言語と静的な言語のどちらが優れているか、という議論は
以前からありますが、あなたはどうお考えでしょうか。
この問題にはさまざまな意見があるかと思いますが、Clojureは動的型付け
を選択した言語です。
しかし、specというライブラリーを使うと、Clojureでも型の恩恵を
受けることができます。
さらに、specでは正規表現のように型を定義することができ、
これが非常に便利です。
また、specで定義した型は
spec/conform
spec/explain
などの関数が適応でき、これらの関数で型の適合性を見ることができます。
準備
specを使用するためには、Clojueのバージョンを1.9.0以上に
宣言する必要があります。
[org.clojure/clojure "1.10.0"]
ネームスペースで
(require '[clojure.spec.alpha :as s])
これで準備が整いました。
基本的な使い方
s/def はキーワードと述語(booleanを返す関数)、あるいはすでに定義された
スペックを引数にとり、新しいスペックを定義します。
例:
(s/def ::name string?)
これで::nameというスペックが定義されました。オブジェクトが
スペックを満たしているかどうかは
s/valid
s/conform
といった関数を使って確かめることができます。
(s/valid? ::name "Tom") ;;=> true
(s/conform ::name "Tom") ;; => true
(s/valid? ::name 1) ;; => false
(s/conform ::name 1) ;; => :clojure.spec.alpha/invalid
正規表現タイプのオペレータでスペックを定義
スペックには
s/cat, s/+, s/*, s/?
などの正規表現(regular expression)を模した関数があり、
正規表現と同じ感覚で型を定義することができます。
例として、
「キーワードと偶数の任意個のペア」
に対応するスペックをこれらの関数を用いて定義してみます。
;; キーワードと偶数が交互に並ぶようなデータ
(s/def ::kwd-even-pairs (s/* (s/cat :k keyword? :e even?)))
(s/conform ::kwd-even-pairs [:a 2 :b 4 :c 6])
;; => [{:k :a, :e 2} {:k :b, :e 4} {:k :c, :e 6}]
関数の引数の型をチェック
specを使うと、静的型付け言語のように関数に渡すオブジェクトの
型に要求をつけることができます。
一つの方法としは、以下のように:pre,:postのアサーションを使うというもの。
(s/def ::name string?)
(s/def ::age pos-int?)
(s/def ::human (s/keys :req [::name ::age]))
;; humanの::nameと::ageを表示する関数。引数が::humanを満たさないとアサートエラー。
(defn human-profile [human]
{:pre [(s/valid? ::human human)]}
(println (format "My name is %s and I'm %d years old." (::name human) (::age human))))
(human-profile {::name "Tanaka" ::age 21}) ;; => My name is Tanaka and I'm 21 years old.
上のコードでは、::humanで定義したスペックに適合したものを
human-profileに渡しています。
human-profileでは:postを使って引数が::humanスペックを満たすように
要求していることに注意して下さい。これにより、::humanスペックに適合しない
オブジェクトをhuman-profileに渡してしまうとエラーになります。
(human-profile {::name "Won" ::age -1})
;;=> 1. Unhandled java.lang.AssertionError
;; Assert failed: (s/valid? :spec-demo/human human)
このようにClojureでも型の恩恵を受けることができるようになりました。
s/explain
さらに、specには
s/explain
という関数があり、スペックに適合しないオブジェクトがあった場合、
「何が原因でスペックに適合しなかったのか」を教えてくれます。
(s/explain ::human {::name "Won" ::age -1})
;; => -1 - failed: pos-int? in: [:spec-demo/age] at: [:spec-demo/age] spec: :spec-demo/age
s/explain-strを使うと、上のようなs/explainの結果を文字列で
返してくれます。s/explain-strを利用すれば、より詳しいエラーメッセージを
自動生成することも可能です。
(defn compile-error [& args]
(throw (new Exception (str "spec error: " (apply format args)))))
;; specに従ってxをconformする関数。xがspecに適合しない場合は例外を投げる.
(defn return-conformed
[spec x]
(if (s/valid? spec x)
(s/conform spec x)
(compile-error "%s is not a valid %s instance.\n \n explanation: \n \n %s"
x spec (s/explain-str spec x))))
先ほどの::keyword-even-pairを使ってテストしてみましょう。
(s/def ::kwd-even-pairs (s/* (s/cat :k keyword? :e even?)))
(return-conformed ::kwd-even-pairs [:a 2 :b 4 :c 6])
;; => [{:k :a, :e 2} {:k :b, :e 4} {:k :c, :e 6}]
;; スペックに適合しないデータを渡してみる。
(return-conformed ::kwd-even-pairs [:a 2 :b 5 :c 6])
;; => spec error: [:a 2 :b 5 :c 6] is not a valid
;; :spec-demo/kwd-even-pairs instance.
;; explanation:
;; 5 - failed: even? in: [3] at: [:e] spec: :spec-demo/kwd-even-pairs
replに詳しいエラーメッセージが表示され、どこがまずかったのかわかりますね。
まとめ
specを使うと以下のメリットがあります。
1 型の適合を確認しながら開発ができる。
2 spec/conformを使ってデータを整理できる。
今回は2についてはあまり述べることができなかったので
次回以降のテーマにしたいと思います。
今回も最後までお読みいただきありがとうございました。