clojure-spec

nikolavojicic 2023-10-16T13:38:22.139159Z

Is it possible to make generator for only some parts of a keys spec and register that as default generator? Example:

(s/def ::foo  int?)
(s/def ::foo- int?)
(s/def ::bar (s/with-gen ;; must be default
               (s/and
                ;; imagine having more keys than this
                (s/keys :req-un [::foo ::foo-])
                ;; foo- depends on foo
                (fn [{:keys [foo foo-]}]
                  (= foo (inc foo-))))
               (fn []
                 (gen/bind (s/gen ::foo)
                           (fn [foo]
                             (s/gen ::bar ;; just override generator for foo-, keep all other
                                    {::foo  (fn [] (gen/return foo))
                                     ::foo- (fn [] (gen/return (dec foo))})))))))
(s/valid? ::bar {:foo 1 :foo- 0}) ;;=> true
(s/valid? ::bar {:foo 1 :foo- 1}) ;;=> false
(gen/generate (s/gen ::bar))      ;;=> StackOverflowError

nikolavojicic 2023-10-16T13:57:33.774659Z

One solution is to repeat spec in s/gen for ::bar, but that is verbose:

(s/def ::bar (s/with-gen
               (s/and
                (s/keys :req-un [::foo ::foo-])
                (fn [{:keys [foo foo-]}]
                  (= foo (inc foo-))))
               (fn []
                 (gen/bind (s/gen ::foo)
                           (fn [foo]
                             (s/gen (s/keys :req-un [::foo ::foo-]) ;; REPEAT
                                    {::foo  (fn [] (gen/return foo))
                                     ::foo- (fn [] (gen/return (dec foo)))}))))))

Alex Miller (Clojure team) 2023-10-16T14:02:31.021889Z

pull the s/keys of bar out into another spec?

👍 1
nikolavojicic 2023-10-16T14:44:23.407729Z

This: (eval (s/form ::bar)) also works:

(s/def ::bar (s/with-gen
               (s/and
                (s/keys :req-un [::foo ::foo-])
                (fn [{:keys [foo foo-]}]
                  (= foo (inc foo-))))
               (fn []
                 (gen/bind (s/gen ::foo)
                           (fn [foo]
                             (s/gen (eval (s/form ::bar))
                                    {::foo  (fn [] (gen/return foo))
                                     ::foo- (fn [] (gen/return (dec foo)))}))))))

Alex Miller (Clojure team) 2023-10-16T14:48:11.810489Z

that does not seem better :)

Alex Miller (Clojure team) 2023-10-16T14:48:44.852929Z

you want to reuse something, so just pull it out and give it a new name

👍 1
nikolavojicic 2023-10-16T14:57:49.597459Z

Yeah, s/form may not be evaluable, e.g.

(eval
 (s/form
  (let [bound 10]
    (s/and int? #(< % bound)))))
;; => Unable to resolve symbol: bound in this context