Hello parking garage, meet clojure.spec

In this post, we will take the parking garage problem from the previous post and see how clojure.spec might be applied. If you are new to clojure.spec you can check out the rationale and the guide on clojure.org. Let’s start by specing the data. Recall that we used a map to state how many parking spaces are available on each garage level:

(ns garage-simulation.core)

(def parking-spaces {0 15
                     1 10})

We could spec the components of this map level and number-of-parking-spaces as positive integers:

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

(s/def ::level (s/and int? (s/or :zero zero? :positive pos?)))
(s/def ::number-of-parking-spaces (s/and int? pos?))

parking-spaces can then be specd like this:

(s/def ::parking-spaces (s/map-of ::level ::number-of-parking-spaces))

We can then check the validity using clojure.spec/valid?:

(s/valid? ::parking-spaces {0 15 1 10})
=> true

If we were to try an invalid map:

(s/valid? ::parking-spaces {0 15 1 "10"})
=> false

If we’re confused as to why our data is not valid, we can use the clojure.spec/explain function to figure out what’s wrong:

(s/explain ::parking-spaces {0 15 1 "10"})
=> In: [1 1] val: "10" fails spec: :garage-simulation.core/number-of-parking-spaces at: [1] predicate: int?

or clojure.spec/explain-data to get a map we can more easily traverse:

(s/explain-data ::parking-spaces {0 15 1 "10"})
=> #:clojure.spec{:problems ({:path [1], :pred int?, :val "10", :via [:garage-simulation.core/parking-spaces :garage-simulation.core/number-of-parking-spaces], :in [1 1]})}

Using this spec we can even generate some valid samples using the clojure.spec.gen/generate or clojure.spec.gen/sample functions:

(require '[clojure.spec.gen :as g]
         '[clojure.spec.test :as t])

(g/generate (s/gen ::parking-spaces))
=> {2868014 31, 3118 28, 46838612 227837, 2631 940026, 109 959746, 275023 58, 61032 3846, 24482 11839, 4370470 92, 1826 412743, 47082 2125773, 190 236375948}
(g/sample (s/gen ::parking-spaces))
=> ({0 1, 2 3} {0 1, 1 1} {0 1, 1 189, 3 2} {1 1, 0 15, 3 1, 2 6} {0 1, 1 5, 4 5, 15 3, 22 6, 6 1, 3 1, 2 2, 8 5} {} {17 10, 5 10, 2 3} {0 7, 30 25, 48 22, 7 117, 10 19, 31 325} {0 2, 1 46, 39 677, 13 69, 6 9, 3 6, 2 712, 23 1, 19 31, 115 21, 5 5} {0 84, 1 2, 15 1, 355 37})

At this point you can see that these samples don’t really match reality because in parking garages levels usually start at 0 and increase consecutively. We could spec this on the map of course, but even better would be to switch to a data structure which already has these constraints i.e. just use a vector :)

(def parking-spaces [15 10])

(s/def ::parking-spaces (s/coll-of ::number-of-parking-spaces :kind vector?))

(g/generate (s/gen ::parking-spaces))
=> [359765 242401325 199244 8685 227771037 10073702 21779006 24302758 347572 6381197 84 1488130 1008076 293347674 135 3109580]
;; might be tough to make it out of this garage :)

Having done this change we will need to tweak intialize-garage! a bit:

(defn initialize-garage!
  "Sets the initial state of the garage."
  [parking-spaces]
  (dosync
   (ref-set vehicles {})
   (ref-set empty-parking-spaces
            (into #{}
                  (for [[level spaces] (map-indexed vector parking-spaces)
                        space-number   (range spaces)]
                    [level space-number])))))

Moving on to licence plates, these are simply strings so we can spec them like this:

(s/def ::licence-plate string?)

There is no global standard for licence plates but if there were we could use a regex to spec the licence plates further. For example if a licence plate were defined as 4 upper case characters followed by 3 digits:

(def licence-plate-regex #"[A-Z]{4}\d{3}")
(s/def ::licence-plate (s/and string? #(re-matches licence-plate-regex %)))

We represented the location for a parking-space using a vector of two integers: [level space]. We can define this as well:

(s/def ::parking-space (s/coll-of (s/and int? (s/or :zero zero? :positive pos?)) :kind vector? :count 2))

Finally we can spec the map used to store the locations of the vehicles in the garage:

(s/def ::vehicles (s/map-of ::licence-plate ::parking-space))

(s/valid? ::vehicles {"ASDF001" [0 3]})
=> true

Now that we’ve specd the data let’s move on to the functions. Starting with initialize-garage! we use clojure.spec/fdef and specify one argument, namely ::parking-spaces:

(s/fdef initialize-garage!
        :args (s/cat :parking-spaces ::parking-spaces))

For locate-vehicle we can also specify the return value:

(s/fdef locate-vehicle
        :args (s/cat :licence-plate ::licence-plate)
        :ret (s/or ::parking-space nil?))

number-of-free-parking-spaces takes no args but we can specify the return:

(s/fdef number-of-free-parking-spaces
        :ret (s/and int? (s/or :zero zero? :positive pos?)))

For enter-garage! we can also specify a relationship between the argument and the return value, namely that the return value should contain the licence plate:

(s/fdef enter-garage!
        :args (s/cat :licence-plate ::licence-plate)
        :ret (s/or ::vehicles nil?)
        :fn #(contains? (:ret %) (-> % :args :licence-plate)))

And similarly for exit-garage! we can specify that the licence plate for the vehicle that just exited the garage should not be present in the return value:

(s/fdef exit-garage!
        :args (s/cat :licence-plate ::licence-plate)
        :ret (s/or ::vehicles nil?)
        :fn #(not (contains? (:ret %) (-> % :args :licence-plate))))

If we turn on instrumentation on a function and then call it with invalid parameters we get an exception with the details of went wrong.

(t/instrument `enter-garage!)

(enter-garage! 3)
 ExceptionInfo Call to #'garage-simulation.core/enter-garage! did not conform to spec:
 In: [0] val: 3 fails spec: :garage-simulation.core/licence-plate at: [:args :licence-plate] predicate: string?
 :clojure.spec/args  (3)
 :clojure.spec/failure  :instrument
 :clojure.spec.test/caller  {:file "form-init6892093246791959693.clj", :line 82, :var-scope garage-simulation.core/eval20779}
   clojure.core/ex-info (core.clj:4724)

This is saying "Sorry you gave me a number but I need a string".

(enter-garage! "ASDF01")
 ExceptionInfo Call to #'garage-simulation.core/enter-garage! did not conform to spec:
 In: [0] val: "ASDF01" fails spec: :garage-simulation.core/licence-plate at: [:args :licence-plate] predicate: (re-matches licence-plate-regex %)
 :clojure.spec/args  ("ASDF01")
 :clojure.spec/failure  :instrument
 :clojure.spec.test/caller  {:file "form-init6892093246791959693.clj", :line 75, :var-scope garage-simulation.core/eval20777}
   clojure.core/ex-info (core.clj:4724)

This says "Sorry your licence plate doesn’t match the regex".

Another thing we can do with our specs is something called property based testing. In unit testing we usually write tests for specific test cases like we did in the previous post with midje. In property based testing we use a framework (in this case test.check) to automatically generate a range of test cases against which the invariants defined in our spec are verified. We do this using the clojure.spec.test/check function:

(clojure.spec.test/check `enter-garage!)
=> ExceptionInfo Couldn't satisfy such-that predicate after 100 tries.  clojure.core/ex-info (core.clj:4724)

Whoops! The problem here is that test.check tried to generate random strings for licence plates for enter-garage! but gave up after a 100 tries because they all did not conform to the regex we defined earlier. This would also happen if we directly tried to generate samples for licence plates:

(g/sample (s/gen ::licence-plate))
=> ExceptionInfo Couldn't satisfy such-that predicate after 100 tries.  clojure.core/ex-info (core.clj:4724)

We can fix this by associating a generator with the spec for licence plates. We can use the test.chuck library for this which provides a handy string-from-regex generator:

(require '[com.gfredericks.test.chuck.generators :as cg])

(s/def ::licence-plate
  (s/with-gen
    (s/and string? #(re-matches licence-plate-regex %))
    #(cg/string-from-regex licence-plate-regex)))

So now we can generate licence plates at will:

(g/sample (s/gen ::licence-plate))
=> ("YZJY672" "WDPR193" "BMAX543" "BIEL908" "VNJC192" "ZKFA361" "HLYS035" "DAIA703" "WFGS654" "LPSX140")

Let’s move on to verifying the invariant for enter-garage!:

(t/check `enter-garage!)
=> ({:spec #object[clojure.spec$fspec_impl$reify__13789 0x1fdaac28 "clojure.spec$fspec_impl$reify__13789@1fdaac28"], :clojure.spec.test.check/ret {:result #error {
  :cause "Specification-based check failed"
  :data {:clojure.spec/problems [{:path [:fn], :pred (contains? (:ret %) (-> % :args :licence-plate)), :val {:args {:licence-plate "SCWE626"}, :ret nil}, :via [], :in []}], :clojure.spec.test/args ("SCWE626"), :clojure.spec.test/val {:args {:licence-plate "SCWE626"}, :ret nil}, :clojure.spec/failure :check-failed}
  :via
  [{:type clojure.lang.ExceptionInfo
    :message "Specification-based check failed"
    :data {:clojure.spec/problems [{:path [:fn], :pred (contains? (:ret %) (-> % :args :licence-plate)), :val {:args {:licence-plate "SCWE626"}, :ret nil}, :via [], :in []}], :clojure.spec.test/args ("SCWE626"), :clojure.spec.test/val {:args {:licence-plate "SCWE626"}, :ret nil}, :clojure.spec/failure :check-failed}
   ...

This doesn’t look good at all :) The reason it happens is that when check is called it generates a large number of inputs which in our case exceeds the available space in the garage. This actually points out a problem in the invariant i.e. the vehicle doesn’t make it into the garage if there is no space available. We can redefine the invariant to accommodate this:

(s/fdef enter-garage!
        :args (s/cat :licence-plate ::licence-plate)
        :ret ::vehicles
        :fn #(or (nil? (:ret %))
                 contains? (:ret %) (-> % :args :licence-plate)))

(t/check `enter-garage!)
=> ({:spec #object[clojure.spec$fspec_impl$reify__13789 0x4d8e87aa "clojure.spec$fspec_impl$reify__13789@4d8e87aa"], :clojure.spec.test.check/ret {:result true, :num-tests 1000, :seed 1469535504589}, :sym garage-simulation.core/enter-garage!})

Much better. Happy specing!