clojure-spec

About: http://clojure.org/about/spec Guide: http://clojure.org/guides/spec API: https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html
Zak Singh 2021-02-19T03:54:55.040500Z

I have a bit of a messy spec/generators question which I’ve written up here - https://stackoverflow.com/questions/66271188/spec-for-map-with-interdependent-values-between-nested-levels - is such a thing possible?

alexmiller 2021-02-19T04:16:07.042800Z

Doing this kind of thing is inherently challenging. The general approach to take is: first generate a model by building up a core and extending it with gen/bind, then at the end generate the actual data structure with gen/fmap

alexmiller 2021-02-19T04:17:39.045300Z

As a simple example, don’t try to generate a square by generating random points. Instead, generate the right and left x, the top and bottom y (that’s your model), then generate the points of the square using fmap at the end

alexmiller 2021-02-19T04:20:02.047Z

So I’m yours, maybe first generate a pool of terminals (collection of more constrained nodes, then randomly pick subsets to combine, and then maybe do some massage at the end

alexmiller 2021-02-19T04:20:21.047700Z

How far you go with this depends how much of the space you want to cover

Zak Singh 2021-02-19T04:22:17.049100Z

Reformulating the problem like that makes a lot of sense - I essentially need to build up layers from the terminal case. This is really an incredibly powerful tool

alexmiller 2021-02-19T04:26:19.050600Z

Make sure to lean on s/gen of specs that may not match your public specs - easiest way to make new sub generators

alexmiller 2021-02-19T04:33:53.051700Z

Like (s/gen (s/tuple ...))

alexmiller 2021-02-19T04:34:51.052800Z

Or (s/gen #{:magic :values})

Zak Singh 2021-02-19T04:36:15.053100Z

(def terminal-gen
  (gen/bind
    (spec/gen (spec/tuple ::terminal-name ::terminal-kind))
    (fn [[name kind]]
      (gen/hash-map
        :name (spec/gen #{name})
        :kind (spec/gen #{kind})))))

Zak Singh 2021-02-19T04:36:27.053400Z

like this?

Zak Singh 2021-02-19T05:51:53.053900Z

Managed to get it built! That was some fun code:

(spec/def ::kind #{"NON_NULL" "LIST" "SCALAR" "OBJECT"})
(spec/def ::name (spec/nilable string?))
(spec/def ::ofType (spec/or :terminal nil?
                            :type ::type))

(spec/def ::terminal-kind #{"SCALAR" "OBJECT"})
(spec/def ::terminal-name string?)

(spec/def ::wrapper-kind #{"NON_NULL" "LIST"})

(def terminal-gen
  (gen/bind
    (spec/gen (spec/tuple ::terminal-name ::terminal-kind))
    (fn [[name kind]]
      (gen/hash-map
        :name (spec/gen #{name})
        :kind (spec/gen #{kind})
        :ofType (gen/return nil)))))

(defn build-type
  ([max-depth] (if (= max-depth 1) terminal-gen
                                   (build-type max-depth 0 terminal-gen)))
  ([max-depth curr-depth inner-gen]
   (if (< curr-depth max-depth)
     (recur max-depth
            (inc curr-depth)
            (gen/bind inner-gen
                      (fn [inner-gen]
                        (if (= "NON_NULL" (:kind inner-gen))
                          (gen/hash-map
                            :name (gen/return nil)
                            :kind (spec/gen #{"LIST"}) ; two NON_NULLs cannot be child-parent
                            :ofType (spec/gen #{inner-gen}))
                          (gen/hash-map
                            :name (gen/return nil)
                            :kind (spec/gen ::wrapper-kind)
                            :ofType (spec/gen #{inner-gen}))))))
     inner-gen)))

(def type-gen
  (gen/bind
    (spec/gen (spec/int-in 1 5))
    build-type))