Prismatic Schema について勉強してみる

***
今まで何度も「使ってみたら面白そうだな～」と思いつつ、目先の忙しさで放置して身につかずだったライブラリ、それが私にとっての *Schema* の位置付けです。Advent Calendar を書くのは、勉強するには絶好の機会だと思い、解説がてら試したこと等整理してみたいと思います（ほとんど自分用メモかもしれません...）
***

# Schema is 何?

Clojure(Script)は Lisp なので、関数の引数、戻り値、変数等々の型宣言は不要です。これはLisperにとっては普通なことではありますが、逆に仕様変更で修正が漏れたことに気づかないまま動いてしまう「動的言語あるある」を誘発してしまうことにもつながります。

[Schema](https://github.com/Prismatic/schema) は、そんな Clojure に「緩い型」をもたらしてくれます。型の定義を宣言的に記述でき、また与えられたデータがその型定義に合致するかの検証をしてくれます。ただし宣言したからといってコンパイルエラーで死ぬというわけではなく、実行時のデータの検証ですので、Clojure の assertion を補完する位置付けとみればよいかと思います。

# まずは初歩から

前置きはこのくらいにしておいて、手を動かしながら学んでいきます。
自分はまだ *leiningen* 派なので、プロジェクトを `lein new schema-study` で作りました。

```project.clj
;;; dependencies に以下を追加(2015/12/12 現在)
  [prismatic/schema "1.0.4"]
```

```src/schema_study/core.clj
(ns schema-study.core
  (:require [schema.core :as s]))

```

## s/validate

*schema* を理解するとっかかりとして、validation を実行する関数 `s/validate` の使い方を見ていきましょう。

```clj
(s/validate s/Bool true)
;; => true
(s/validate s/Bool nil)
;; => ExceptionInfo Value does not match schema: (not (instance? java.lang.Boolean nil))  schema.core/validator/fn--611 (core.clj:151)
```
validate は、第１引数に指定された `schema` に対して、第２引数の値が合致するか、を検証します。検証で合致した場合には第２引数の値をそのままスルーして返します。上記例では、`schema/Bool` を指定しているので、`true` or `false` 以外を指定した２番目の例は、例外を出してしまいます。

単一のスカラ値に対する schema としては、`s/Any`、`s/Bool`、`s/Num`、`s/Keyword`、`s/Symbol`、`s/Int`、`s/Str` があります。これらは Schema ライブラリの中で定義済の cross platform な `schema` ですが、(ClojureScript ではない)Clojure の場合、`s/validate` の第１引数には JVM の class を指定することができますので、例えば

```clj
(s/validate double 1.0)
;; => 1.0
(s/validate java.math.BigDecimal 99999999999999.999M)
;; => 99999999999999.999M
```

等も可能です。正規表現で簡単にチェックしたい時も大丈夫。

```clj
(s/validate #"^\d{4}$" "0123")
;; => "0123"
(s/validate #"^\d{4}$" "01234")
;; => ExceptionInfo Value does not match schema: (not (re-find #"^\d{4}$" "01234"))  schema.core/validator/fn--611 (core.clj:151)
```

また、よく使いそうな感じなものとして `s/enum` もあります。

```clj
(def hoge-code "01")    ; データベースに格納されていそうなコード値(とり得る値は01,02,03)
(s/validate (s/enum "01" "02" "03") hoge-code)
;; => 01
(def bad-code "04")
(s/validate (s/enum "01" "02" "03") bad-code)  ;; ダメなコード
;; => ExceptionInfo Value does not match schema: (not (#{"03" "02" "01"} "04"))  schema.core/validator/fn--611 (core.clj:151) 
```

次に collection も見ていきましょう。

```clj
;; array
(s/validate [s/Str] ["a" "b" "c"])
;; => ["a" "b" "c"]
;; map
(s/validate {s/Keyword s/Str} {:k "v"})
;; => {:k "v"}
;; set
(s/validate #{s/Str} #{"v1" "v2"})
;; => #{"v1" "v2"}
;;  :
```

collection の場合は、その要素の `schema` もちゃんと見てくれます。以下は例外を出す例です。

```clj
(s/validate #{s/Str} #{1 "v2"})
;; => ExceptionInfo Value does not match schema: #{(not (instance? java.lang.String 1))}  schema.core/validator/fn--611 (core.clj:151)
(s/validate {s/Keyword s/Num} {:a 1 :b 2 :c "3"})
;; => ExceptionInfo Value does not match schema: {:c (not (instance? java.lang.Number "3"))}  schema.core/validator/fn--611 (core.clj:151)
```

## 組み合わせてみたい

ところで `schema` の実体は、というとただの Clojure のデータだったりします。そこで `schema` に名前をつけて *定義* したいときには、普通に `def` してしまえばよいことがわかります。

```clj
(def KeywordNumber {s/Keyword s/Num})
KeywordNumber 
;; => {Keyword java.lang.Number}
(s/explain KeywordNumber)
;; => {Keyword Num}
;; 定義した KeywordNumber を使ってみる
(s/validate KeywordNumber {:a 1 :b 2 :c 3})
;; => {:a 1, :b 2, :c 3}
```

clojure のデータ型なので気軽に定義したり利用したりできますね。さらに、`schema`を組み合わせて利用するための小道具もいろいろあります。まずは `s/optional-key`と`s/required-key` 。

```clj
(def OptionalAandB {(s/optional-key :a) s/Num :b s/Str})
(s/validate OptionalAandB {:b "b"})       ; :a がない
;; => {:b "b"}
(s/validate OptionalAandB {:a 1 :b "b"})  ; :a があり値がNum
;; => {:a 1, :b "b"}
(s/validate OptionalAandB {:a 1 :b "b" :c 2})  ; :c ？知らない子ですね
;; => ExceptionInfo Value does not match schema: {:c disallowed-key}  schema.core/validator/fn--611 (core.clj:151)
(s/validate OptionalAandB {:a "str" :b "b"})   ; :a があり値が文字列
;; => ExceptionInfo Value does not match schema: {:a (not (instance? java.lang.Number "str"))}  schema.core/validator/fn--611 (core.clj:151)

(def RequiredA {(s/required-key :a) s/Num s/Keyword s/Num})
(s/validate RequiredA  {:a 1})          ; :a は必須
;; => {:a 1}
(s/validate RequiredA  {:b 1})          ; 必須の :a がない
;; => ExceptionInfo Value does not match schema: {:a missing-required-key}  schema.core/validator/fn--611 (core.clj:151)
(s/validate RequiredA  {:a 1 :b 2})     ; :a 以外に s/Keyword s/Num を満たすものがある
;; => {:a 1, :b 2}
(s/validate RequiredA  {:a 1 :b "2"})   ; :a 以外がダメ
;; => ExceptionInfo Value does not match schema: {:b (not (instance? java.lang.Number "2"))}  schema.core/validator/fn--611 (core.clj:151)
```

`schema` を組み合わせる系を以下に少し紹介しておきます。

```clj
;;; maybe
(s/validate (s/maybe s/Keyword) :a)
;; => :a
(s/validate (s/maybe s/Keyword) nil)
;; => nil

;;; pred
(s/validate (s/pred pos?) -123)
;; => ExceptionInfo Value does not match schema: (not (pos? -123))  schema.core/validator/fn--611 (core.clj:151)
(s/validate (s/pred pos?) 123)
;; => 123

;; その他 s/optional, s/conditional, s/if, ... だんだん疲れてきたので省略
```

## s/defrecord

実際に大規模のアプリケーションを作る上で最も *schema* の恩恵を受けるのは、`record` ではないかと思います。`clojure.core/defrecord` の代わりに、`s/defrecord`を使うことで、`record`の各メンバーの型(schema)を(`:-`の直後に)指定することができます。

```clj
(s/defrecord User
    [id :- Long
     name :- s/Str])
(s/explain User)
;; => (record schema_study.core.User {:id java.lang.Long, :name Str})
(s/validate User (map->User {:id 1 :name "a"}))
;; => #schema_study.core.User{:id 1, :name "a"}
(s/validate User (map->User {:id "a" :name 1}))
;; => ExceptionInfo Value does not match schema: {:id (not (instance? java.lang.Long "a")), :name (not (instance? java.lang.String 1))}  schema.core/validator/fn--611 (core.clj:151)
```

レコードの各メンバー毎に固有の性質を与える(上記でいう`Long`とか`s/Str`)には、普通に`:-`で*schema*を設定することで対応できます。しかし用途によっては設定したい制約条件が一つのメンバーのみで決められない場合もあるかと思います。こういったケースだと`s/defrecord` の `extra-validator-fn` で実現できるようです([ayato_pさんの記事](http://ayato-p.github.io/blog/2015/07/14/extra_validator_function.html)が詳しくてわかりやすくてオススメです)。

## 関数に対する schema 指定

*schema* では、関数定義時に `schema`指定をする `s/defn` というものがあります。関数名の直後に関数の戻り値の `schema` を、[]の引数各々の後に各引数の `schema` を `:-` に続けて書くことで指定します。

```clj
(def PositiveDouble (s/constrained double pos?))  ;; positive number
(s/defn sqrt :- PositiveDouble
  [v :- PositiveDouble]
  (Math/sqrt v))
;; s/fn-schema で定義を確認してみる
(s/explain (s/fn-schema sqrt))
;; => (=> (constrained double pos?) (constrained double pos?))
;; validation なしで実行
(sqrt -4.0)
;; => NaN
;; validation ありで実行
(s/with-fn-validation (sqrt -4.0))
;; => ExceptionInfo Input to sqrt does not match schema: [(named (not (pos? -4.0)) v)]  schema-study.core/eval14321/sqrt--14326 (form-init3371089079384472141.clj:233)
(s/with-fn-validation (sqrt 4.0))
;; => 2.0
```

## validation の実行とテスト

*Schema* では validation は上記のように `s/with-fn-validation` マクロの中で実行されますが、逆に言えば `s/with-fn-validation` を使わないと validation が実行されません。実際の開発では validation を厳密に行い素早くバグを取り、実運用に入る前にパフォーマンスを上げるため validation はとっぱらいたい、といったケースに対応するには、テストコードに細工を入れる、という手があるようです。

```clj
(use-fixtures :once schema.test/validate-schemas)
```
## おわりに

*Schema* のドキュメントを見るとまだまだ試していないことがたくさんあります。が、手を広げすぎると一冊の本ができあがりそうな感じがしました（単に自分がまとめるのが下手なだけ、かもしれませんが）。浅いながらも大体の感じはつかめたので、今後自分の業務のなかで応用していけたらなぁ、と思います。