たとえば、「整数をとる:x
というキー」と「文字列をとる:y
というキー」をもつマップのスペックは以下のように表現できる:
(s/def ::x integer?)
(s/def ::y string?)
(s/keys :req-un [::x ::y])
;; (s/valid? (s/keys :req-un [::x ::y]) {:x 1})
;; => false
;; 両方のキーがないとダメ
もし、「:x
と:y
の少なくともどちらか一方をもつ」という条件ならこう書ける:
(s/keys :req-un [(::x ::y)]
;; (s/valid? (s/keys :req-un [(or ::x ::y)]) {:x 1})
;; => true
;; 片方のキーだけでもOK
:req
や:req-un
のベクタ内では、or
だけでなくand
も使えるようになっていて、キーの複雑な存在条件を記述することだできるようになっている。
この機能はあまり知られていないがs/keys
のdocstringにもちゃんと書いてある:
The :req key vector supports 'and' and 'or' for key groups:
(s/keys :req [::x ::y (or ::secret (and ::user ::pwd))] :opt [::z])
ところで、上の例ではor
を使っているので、当然:x
も:y
も両方とも持つ場合もいいことになる:
(s/valid? (s/keys :req-un [(or ::x ::y)]) {:x 1 :y "foo"})
;; => true
たまに聞く話として「どちらか一方のキーのみをもつ場合しか受けつけたくないときはどうすればいいんだ?」というのがあるようだ (個人的には、今のところそういうスペックがほしいと思うケースに遭遇したことはない)。
そのような場合には、以下のようなスペックを書くのが常套的かと思う:
(s/and (s/keys :req-un [(or ::x ::y)])
(fn [m] (or (and (contains? m :x) (not (contains? m :y)))
(and (not (contains? m :y)) (contains? m :y)))))
つまり、s/keys
だけでは所望の条件を表現できないので、s/and
ないしs/merge
で追加の条件をつけてやるという話。
そういうものかと納得してしまえばそれまでの話なんだけど、たまたまs/keys
の実装を読んでいたら、
s/keys
内で使えるand
やor
の位置には、実はそのスコープで見えてる任意の関数・マクロが使えるということのようだ。
これは単に今の時点でs/keys
の構文チェックを厳密にしていないための不具合というか未定義動作なので、この挙動を利用して
何をかしようとするのはまったくもってお薦めしないのだけど、この挙動を使えばさっきの例はs/keys
の中でしれっとnot
を使ってやることで
以下のようにすっきりと書くことができる:
(s/keys :req-un [(or (and ::x (not ::y)) (and (not ::x) ::y))])
;; 両方のキーがあるとダメ
;; (s/valid? (s/keys :req-un [(or (and ::x (not ::y)) (and (not ::x) ::y))]) {:x 1 :y "foo"})
;; => false
;; 片方のキーのみならOK
;; => false
;; (s/valid? (s/keys :req-un [(or (and ::x (not ::y)) (and (not ::x) ::y))]) {:x 1})
;; => true
;; (s/valid? (s/keys :req-un [(or (and ::x (not ::y)) (and (not ::x) ::y))]) {:y "foo"})
;; => true