clojure.spec入門1

clojure.specはClojureでデータ、関数、マクロなどの仕様を書くためのライブラリーです。 clojure.specで仕様を書くことによる利点はいろいろありますが、まずは深く考えずにclojure.specで遊びましょう。

なお、この文書ではclojure.spec名前空間sという別名を付けています。 また、この文書はClojure 1.9.0-alpha7の使用を前提としています。

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

clojure.specでは仕様をスペックオブジェクトで表します。 スペックオブジェクトはs/specマクロに述語関数*1を渡すことで作れます。 次の例では「整数である」という仕様を表すスペックオブジェクトを作ります。

(s/spec integer?)

ある値が仕様を満たしているかどうかを判断するにはs/conform関数を使います。 次の例では100.2がそれぞれ「整数である」という仕様を満たしているかどうかを判断します。

(s/conform (s/spec integer?) 10)
;;=> 10
(s/conform (s/spec integer?) 0.2)
;;=> :clojure.spec/invalid

s/conformは第1引数の仕様を第2引数の値が満たしていないときに:clojure.spec/invalidを返します。 第1引数を第2引数が満たしているときにs/conformが何を返すかは第1引数の種類によります(この例では第2引数をそのまま返しています)。

前述の例は実は次のように書くこともできます。

(s/conform integer? 10)
;;=> 10
(s/conform integer? 0.2)
;;=> :clojure.spec/invalid

これはs/conformの第1引数がスペックオブジェクトよりも広い概念である「述語(predicates)」を受け入れるようになっているからです。 述語には次のものが含まれます。

  • 述語関数
  • セット
  • 登録された述語に付けられた名前
  • スペックオブジェクト
  • 正規表現演算子

clojure.spec内の関数、マクロの多くはs/conformと同様に述語を受け入れるようにできています。 そのため、s/specを使って明示的にスペックオブジェクトを作ることはあまりありません。


ある値が「整数かつ正の数である」という仕様を満たしているかどうかを判断するには次のように書きます。

(s/conform (s/and integer? pos?) 20)
;;=> 20
(s/conform (s/and integer? pos?) -3)
;;=> :clojure.spec/invalid

s/andマクロは引数として渡された複数の述語を「かつ」で結合します。 s/andの返り値はスペックオブジェクトです。

ある値が仕様を満たしていないとき、仕様のどの部分を満たしていないのかを知りたい場合にはs/explain関数を使います。

(s/explain (s/and integer? pos?) -3)
;;=> val: -3 fails predicate: pos?
(s/explain (s/and integer? pos?) 0.1)
;;=> val: 0.1 fails predicate: integer?

なお、前述の例は次のようにも書けます。

(s/conform #(and (integer? %) (pos? %)) 20)
;;=> 20
(s/conform #(and (integer? %) (pos? %)) -3)
;;=> :clojure.spec/invalid

しかし、このように書いてしまうと仕様を満たしていないときに仕様のどの部分を満たしていないのかを知ることが難しくなります。

(s/explain #(and (integer? %) (pos? %)) 0.2)
;;=> val: 0.2 fails predicate: :clojure.spec/unknown

複数の述語を「かつ」で結合するs/andに対して複数の述語を「又は」で結合するs/orマクロもあります。

(s/conform (s/or :s string? :b boolean?) true)
;;=> [:b true]
(s/conform (s/or :s string? :b boolean?) "Clojure")
;;=> [:s "Clojure"]

s/orでは引数をキーワードと述語とのペアで書きます。 s/orの返り値はスペックオブジェクトです。

s/orを使った場合のs/conformの返り値に注目しましょう。 s/orを使った場合、s/conform分配束縛を行い、その返り値はキーワードと値とのペアのベクターになります。

なお、s/andは引数として渡された述語を前から順番に検討していきますが、ある述語について仕様を満たすと判断した場合、その述語にs/conformを適用したときの返り値がそれ以降の述語に渡されます。

つまり、次のようなことが起こります。

(s/conform (s/and (s/or :s string? :b boolean?) string?) "Clojure")
;;=> :clojure.spec/invalid

s/andの第1引数の述語を検討した後、第2引数の述語に渡されるのは"Clojure"ではなく[:s "Clojure"]になります。

(s/conform (s/and (s/or :s string? :b boolean?) vector?) "Clojure")
;;=> [:s "Clojure"]

「第1要素が整数かつ第2要素が小数であるシーケンスである」という仕様を表すにはどうすればよいでしょうか。 まずは次の方法が考えられます。

(s/conform (s/and #(pos? (first %)) #(neg? (second %))) '(1 -2))
;;=> (1 -2)

しかし、s/catマクロを使うともっとよい感じに書くことができます。 s/catでは引数をキーワードと述語とのペアで書きます。

(s/conform (s/cat :p pos? :n neg?) '(1 -2))
;;=> {:p 1, :n -2}

s/catは渡された引数の述語を連結し、シーケンスにマッチする「正規表現演算子regex ops)」を作ります。 clojure.specではs/cats/altマクロ、s/*マクロ、s/+マクロ、s/?マクロ、s/&マクロの返り値を正規表現演算子と呼びます。 s/catを使った場合、s/conform分配束縛を行い、その返り値はキーワードと値とのペアのマップになります。

なお、s/cats/andを組み合わせると、次のようなこともできます。

(s/conform (s/and (s/cat :i integer? :f float?)
                  #(< (:i %) (:f %))) '(3 6.4))
;;=> {:i 3, :f 6.4}

このような形で「第1要素が整数かつ第2要素が小数かつ第1要素が第2要素よりも小さいシーケンスである」という仕様を表すことができます。

(「clojure.spec入門2」に続く)

参考資料

*1:この文書では引数を1つ受け取り、ブール値を返す関数を述語関数と呼びます