Created
June 27, 2017 16:50
-
-
Save levand/dcbf553bac2b433fe42ce08b6dc2c11c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; 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