Any tips for optimizing specs for validation?

I’m using spec to validate updates before saving to a DB - basically updated/created entities have to match the schema for their type before I attempt the actual db write. I found that this validation stage was slow, and made some changes to the specs and saw huge perf gains. I’m wondering whether there’s any guidance online for writing efficient specs, or whether anyone has any tips.

As an example, here’s the change I made, which in hindsight is pretty obvious.

;; Domain entities have an :entity/type key indicating their type.
;; It's a survey-building app, so the types are :entity.type/question,
;; :entity.type/survey, etc.
(defn constrain-type [t]
  (fn [x] (= (:entity/type x) t)))

;; Initial version 
(s/def ::question
  (s/and
   (s/keys :req [...])
   (constrain-type :entity.type/question)))

(s/def ::survey-content
  (s/or :q ::question
        :p ::page))

;; Way faster version
(s/def ::question
  (s/and
   ;; next two lines are swapped from initial version
   (constrain-type :entity.type/question)
   (s/keys :req [...])))

It makes sense that the second version is faster; ::question and ::page have similar specs, so putting constrain-type first makes it so we can rule out non-matches faster.

For this particular type of spec with a ‘type’ field indicating what the rest of the map looks like, have a look at multi-specs in the spec guide.

1 Like

Thank you for the tip. This is pretty close to what I want, with the exception that there doesn’t appear to be any built-in way to check that a map matches the schema of a particular “subtype”. For example, here’s the schema for an art museum:

(require '[clojure.spec.alpha :as s])

(s/def :artwork/type keyword?)
(s/def :artwork/maker string?)
(s/def :artwork/year integer?)
(s/def :painting/palette (s/coll-of string?))

(defmulti artwork-type :artwork/type)

(defmethod artwork-type :painting [_]
  (s/keys :req [:artwork/type :artwork/maker :artwork/year :painting/palette]))

(defmethod artwork-type :sketch [_]
  (s/keys :req [:artwork/type :artwork/maker :artwork/year]))

(s/def ::artwork (s/multi-spec artwork-type :artwork/type))

(s/explain-data ::artwork {:artwork/type :sketch
                           :artwork/maker "tom"
                           :artwork/year 2020}) ;; => nil

(s/explain-data ::artwork {:artwork/type :painting
                           :artwork/maker "tom"
                           :artwork/year 2020
                           :painting/palette ["blue"]}) ;; => nil

All of the above is good, but how do I check that a map describes not just an artwork, but a painting? As far as I can tell, we still have to resort to something similar to the example in my first post:

(s/def ::painting
  (s/and #(= (:artwork/type %) :painting)
         ::artwork))

(s/explain-data ::painting {:artwork/type :painting
                            :artwork/maker "tom"
                            :artwork/year 2020
                            :painting/palette ["blue"]}) ;; => nil

I would pull the specs for painting and sketch into top level specs, and then return those from the multimethod.

e.g.

    (s/def ::painting (s/keys :req [:artwork/type :artwork/maker :artwork/year :painting/palette])
    (s/def ::sketch (s/keys :req [:artwork/type :artwork/maker :artwork/year]))

    (defmulti artwork-type :artwork/type)
    (defmethod artwork-type :painting [_] ::painting)
    (defmethod artwork-type :sketch [_] ::sketch)
    (s/def ::artwork (s/multi-spec artwork-type :artwork/type))

Ah, that makes sense. Thank you!