7

The following clojure spec ::my permits maps having either the key :width or the key :height, however it does not permit having both of them:

(s/def ::width int?)

(s/def ::height int?)

(defn one-of-both? [a b]
  (or (and a (not b))
      (and b (not a))))

(s/def ::my (s/and (s/keys :opt-un [::width ::height])
                   #(one-of-both? (% :width) (% :height))))

Even if it does the job:

(s/valid? ::my {})
false
(s/valid? ::my {:width 5})
true
(s/valid? ::my {:height 2})
true
(s/valid? ::my {:width 5 :height 2})
false

the code does not appear that concise to me. First the keys are defined as optional and then as required. Does anyone have a more readable solution to this?

4

3 回答 3

10

clojure.spec is designed to encourage specs capable of growth. Thus its s/keys does not support forbidding keys. It even matches maps having keys that are neither in :req or opt.

There is however a way to say the map must at least have :width or :height, i.e. not XOR, just OR.

(s/def ::my (s/keys :req-un [(or ::width ::height)]))
于 2017-01-27T19:27:29.670 回答
6

This feature is built into spec - you can specify and/or patterns in req-un:

(s/def ::my (s/keys :req-un [(or ::width ::height)]))
:user/my
user=> (s/valid? ::my {})
false
user=> (s/valid? ::my {:width 5})
true
user=> (s/valid? ::my {:height 2})
true
user=> (s/valid? ::my {:width 5 :height 2})
true
于 2017-01-27T19:29:34.533 回答
1

Just wanted to pitch in with a small modification to the spec in the original question which logic will fail if the value held by any key is falsey, i.e. false or nil.

(spec/valid? ::my {:width nil})
=> false

Of course this won't occur with the given constraints of int? put on the values in the question. But perhaps someone in posterity allows their values to be nilable or boolean in which case this answer becomes handy.

If we instead define the spec as:

(defn xor? [coll a-key b-key]
  (let [a (contains? coll a-key)
        b (contains? coll b-key)]
    (or (and a (not b))
        (and b (not a)))))

(spec/def ::my (spec/and (spec/keys :opt-un [::width ::height])
                         #(xor? % :width :height)))

we get the result that

(spec/valid? ::my {:width nil})
=> true
(spec/valid? ::my {:width nil :height 5})
=> false
于 2017-04-12T15:42:49.203 回答