Skip to content

Instantly share code, notes, and snippets.

@levand
Created June 27, 2017 16:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save levand/dcbf553bac2b433fe42ce08b6dc2c11c to your computer and use it in GitHub Desktop.
Save levand/dcbf553bac2b433fe42ce08b6dc2c11c to your computer and use it in GitHub Desktop.
;; Say we are writing a model of a zoo, and most of our code depends on
;; values like this:
(s/def :zoo/animal (s/keys :req [:zoo.animal/name
:zoo.animal/num-legs]))
(s/def :zoo.animal/name string?)
(s/def :zoo.animal/num-legs (s/and integer? (complement neg?)))
;; In some of our code, we know we're dealing with a more specific type of
;; animal. Spiders must have all the attributes that animals do, plus some
;; additional attributes.
(s/def :zoo/spider (s/merge :zoo/animal
(s/keys :req [:zoo.spider/web])))
(s/def :zoo.spider/web #{:spiral :tangle :funnel :none})
;; And spiders validate as we would expect:
(s/valid? :zoo/spider
{:zoo.animal/name "Rose the Tarantula"
:zoo.animal/num-legs 8
:zoo.spider/web :none}) ; => true, Rose is a spider!
(s/valid? :zoo/spider
{:zoo.animal/name "Rover"
:app.dog/breed "Hound"
:zoo.animal/num-legs 4}) ; => false, Rover is a dog, not a spider!
;; The problem: how do we we restrict the :zoo.animal/num-legs attribute
;; to be 8 in the case of spiders? In other words, how do we prevent this
;; from validating?
(s/valid? :zoo/spider
{:zoo.animal/name "Steve the Mutant"
:zoo.animal/num-legs 13
:zoo.spider/web :none})
;; First, some common suggestion that *won't* work:
;;
;; s/multi-spec:
;; Multi spec could be used change the spec we use
;; to validate an animal based on a :zoo.animal/species key (for example),
;; but it can't change the previously defined spec for :zoo.animal/num-legs
;; s/and:
;; s/and can be used to combine specs, and if we were validating *just*
;; the number 13, that'd be fine: we could use `(s/and :zoo.animal/num-legs
;; #{8})`. But usually we aren't in this position: we usually want to validate
;; the whole animal, map not just the value of the legs attribute. s/and can't
;; help us here, because again, we can't override the previously defined global
;; value of :zoo.animal/num-legs.
;; The best existing workaround is to use a custom predicate, and apply it
;; to the :zoo/spider spec at the top level:
(s/def :zoo/spider (s/and (s/merge :zoo/animal
(s/keys :req [:zoo.spider/web]))
#(-> % :zoo.animal/num-legs #{8})))
;; Steve the mutant, above, will now fail to validate as a spider. However,
;; this has a number of drawbacks. It becomes impossible to efficiently
;; generate valid values, and `explain` is far less useful because the only
;; information known is that the predicate failed, not why. This also means
;; that s/conform can't be used to While this isn't
;; a huge issue for this specific example, it is a problem in general (e.g,
;; what if the value of num-legs was a complex collection instead of just a
;; simple integer?)
;; The proposed solution from Rich is to add an 'override' option to
;; s/valid?, s/conform and s/explain, and then use that with our first
;; :zoo/spider spec:
(s/def :zoo/spider (s/merge :zoo/animal
(s/keys :req [:zoo.spider/web])))
;; EXAMPLE: does not currently work
(s/valid? :zoo/spider
{:zoo.animal/name "Steve the Mutant"
:zoo.animal/num-legs 13
:zoo.spider/web :none}
{:zoo.animal/num-legs #{8}})
;; I am exploring some similar solutions, but relating to adding a conformable,
;; explainable, gen-able restriction on the spec itself:
(s/def :zoo/spider
(s/restrict (s/merge :zoo/animal
(s/keys :req [:zoo.spider/web]))
{:zoo.animal/num-legs #{8}}))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment