[Clojure]spec紹介

Clojureは動的言語なので、型の情報なしで開発を進めていくのが基本です。

動的な言語と静的な言語のどちらが優れているか、という議論は
以前からありますが、あなたはどうお考えでしょうか。

この問題にはさまざまな意見があるかと思いますが、Clojureは動的型付け
を選択した言語です。

しかし、specというライブラリーを使うと、Clojureでも型の恩恵を
受けることができます。

さらに、specでは正規表現のように型を定義することができ、
これが非常に便利です。

また、specで定義した型は

spec/conform

spec/explain

などの関数が適応でき、これらの関数で型の適合性を見ることができます。

##準備

specを使用するためには、Clojueのバージョンを1.9.0以上に
宣言する必要があります。

```Clojure
[org.clojure/clojure "1.10.0"]
```

ネームスペースで

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

これで準備が整いました。

## 基本的な使い方

s/def はキーワードと述語（booleanを返す関数)、あるいはすでに定義された
スペックを引数にとり、新しいスペックを定義します。

例：

```Clojure
  (s/def ::name string?)
```

これで::nameというスペックが定義されました。オブジェクトが
スペックを満たしているかどうかは

s/valid

s/conform

といった関数を使って確かめることができます。

```Clojure
  (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)を模した関数があり、
正規表現と同じ感覚で型を定義することができます。

例として、

「キーワードと偶数の任意個のペア」

に対応するスペックをこれらの関数を用いて定義してみます。

```Clojure
;; キーワードと偶数が交互に並ぶようなデータ
  (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のアサーションを使うというもの。

```Clojure
  (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に渡してしまうとエラーになります。

```Clojure
(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

という関数があり、スペックに適合しないオブジェクトがあった場合、
「何が原因でスペックに適合しなかったのか」を教えてくれます。

```Clojure
(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を利用すれば、より詳しいエラーメッセージを
自動生成することも可能です。

```Clojure
(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を使ってテストしてみましょう。

```Clojure
(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を使うと以下のメリットがあります。

 １　型の適合を確認しながら開発ができる。

 ２　spec/conformを使ってデータを整理できる。

 今回は2についてはあまり述べることができなかったので
 次回以降のテーマにしたいと思います。

 今回も最後までお読みいただきありがとうございました。

## 参考サイト

[Clojure公式サイトのスペック解説ページ](https://clojure.org/guides/spec)