3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[Clojure]spec紹介

Posted at

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についてはあまり述べることができなかったので
次回以降のテーマにしたいと思います。

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

参考サイト

Clojure公式サイトのスペック解説ページ

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?