Clojure.Spec Workshop
Part 1 - The Foundations

qr code
ClojureRemote - February 2017
Yehonathan Sharvit

		  (str (js/Date.))
              

Who am I?

  • A mathematician
  • A pragmatic theorist
  • A coder
  • A freak of interactivity
  • Founded Audyx - an Audiology Startup with 30K LOCs in Clojurescript
  • Author of KLIPSE
  • A Clojure consultant

Agenda

  • Clojure.Spec: What's the big deal?
  • Basic concepts: conform/validate/explain
  • Deeper concepts: sample generation
  • Amazing stuff: unit test generation

Clojure.Spec: What's the big deal?

KLIPSE - Code interactivity

Open this presentation in your computer for a collaborative live coding session: http://slides.klipse.tech/clojure-spec-cr17/part1.html#slide-3

(It works also on mobile)

qr code

The interactive code snippets are powered by KLIPSE. 🤗


	    (map inc [1 2 3])
	

Clojure.spec - The foundations

Most significant part of Clojure 1.9.

clojure.spec features:

  • specification of the structure (the shape) of data
  • validation of data
  • destructuring data
  • data generation based on the specs
  • tests generation based on the specs

All you have to do is to require clojure.spec like this:


	      (ns foo.core
	      (:require [clojure.spec.alpha :as s]))
	  

Data structure specification


	    (s/def ::id integer?)
	    (s/def ::name string?)
	

Any predicate can be passed to the spec definition:

(A predicate is a function that returns a boolean)


	      (s/def ::my-big-integer (fn [x] (and (integer? x)
	      (> x 1000000))))
	  

	      (s/def ::my-short-string (fn [x] (and (string? x)
	      (< (count x) 5))))
		 

Interlude - Namespaced Keywords

Pay attention to the ::!
Spec definitions must used namespace keywords


	    (s/def :a-big-integer (fn [x] (and (integer? x)
	    (> x 1000000))))
	  

Namespaced keywords are like keywords but they are namespaced

The double colon is a syntactic sugar for the current namespace:


		::hello
	    

Mandatory in clojure.spec because specs are registered in a global registry!

Data structure validation

Validate data with spec


	    (s/valid? ::id 19)
	

	    (s/valid? ::id "my-id")
	

And with one of our custom predicates:


	      (s/valid? ::my-big-integer "abc")
	  

	      (s/valid? ::my-big-integer 17)
	  

Wouldn't it be great if we could get an explanation about what parts of the spec were not satisfied?

Data structure validation with explanations

Explain validation


	    (s/explain-str ::my-big-integer "abc")
	

Not so useful 😕

Custom predicates - s/and

Big Integer - the real way


            (s/def ::big-integer (s/and integer?
            #(> % 1000000)))
	

When it's not an integer:


	      (s/explain-str ::big-integer "abc")
	  

When it's a small integer:


	      (s/explain-str ::big-integer 42)
	  

Much better 😄

Short String - the real way


              (s/def ::short-string (s/and string?
              #(< (count %) 5)))
		  

When it's not a string:


	      (s/explain-str ::short-string 42)
	  

When it's a long string:


	      (s/explain-str ::short-string "Hello World!")
	  

Custom predicates - s/or

We must annotate each branch with a tag


	    (s/def ::big-integer-or-short-string (s/or :int ::big-integer
	    :str ::short-string))
	

That makes the explanations about the failure really clear:


            (s/explain-str ::big-integer-or-short-string :hello-world!)
	

Data parsing with s/conform

Conform is a fancy term for data parsing according to a spec

With primitives, the conformed data is the same as the original data


(s/conform ::id 4200000)
	  

With non-primitives, the conformed data is parsed into a data structure with information about the data

When it's a big integer:


              (s/conform ::big-integer-or-short-string 4200000)
	  

When it's a small string:


              (s/conform ::big-integer-or-short-string "abc")
	  

When it's neither this nor that:


              (s/conform ::big-integer-or-short-string :hello-world!)
	  

It works well with a nested spec


	      (s/def ::my-special-spec (s/or :keyword keyword?
	      :bioss ::big-integer-or-short-string))
              (s/conform ::my-special-spec "aa")
	  

Entity maps

You describe the structure of your maps by combining the specs of its keys
and specifying what keys are required and what keys are optional


	      (s/def ::my-map (s/keys :req [::big-integer]
              :opt [::short-string]))
	  

This one is valid:


	      (s/explain-str ::my-map {::big-integer 90000000
              ::short-string "Hell"})
	  

This one is invalid for 2 reasons:


	      (s/explain-str ::my-map {::big-integer 90
              ::short-string "Hello World!"})
	  

What about this one?


	      (s/valid? (s/keys) {::big-integer-or-short-string 90})
	  

It's a bit surprising but in spec, ALL the namespace-qualified keys are validated by any registered specs!

Why do we need opt?

Entity maps - unnamespaced keys

You can also have unspaced keywords


	      (s/def ::my-map-un (s/keys :req-un [::big-integer]
              :opt-un [::short-string]))
	  

This one is valid:


	      (s/explain-str ::my-map-un {:big-integer 90000000
              :short-string "Hell"})
	  

This one is invalid for 2 reasons:


	      (s/explain-str ::my-map-un {:big-integer 90
              :short-string "Hello World!"})
	  

What about this one?


	      (s/valid? (s/keys) {:big-integer-or-short-string 90})
	  

Only the namespace-qualified keys are validated by any registered specs!

Sequences - Regular Expressions on steroids!

You can desribe the shape of a data sequence using Regular Expressions operators:

  • cat - concatenation of predicates/patterns
  • alt - choice among alternative predicates/patterns
  • * - 0 or more of a predicate/pattern
  • + - 1 or more of a predicate/pattern
  • ? - 0 or 1 of a predicate/pattern

An example:


	      (s/def ::employee (s/cat :name (s/alt :full string?
              :first-and-last (s/tuple string? string?))
              :salary ::big-integer))
	  

	      (s/explain-str ::employee '("John Woo" 999))
	  

	      (s/conform ::employee '(["John" "Woo"] 9999999))
	  

Sequences - Regular Expressions on steroids! (cont.)

You can describe the shape of a data sequence using Regular Expressions operators:

  • cat - concatenation of predicates/patterns
  • alt - choice among alternative predicates/patterns
  • * - 0 or more of a predicate/pattern
  • + - 1 or more of a predicate/pattern
  • ? - 0 or 1 of a predicate/pattern

A simplified version of the args of defn:


	      (s/def ::defn-args (s/cat :name symbol?
              :docstring (s/? string?)
              :args (s/coll-of symbol? :kind vector?)))
	  

With a doc string : (defn foo "foo receives two arguments" [a b])


		(s/conform ::defn-args ['foo "foo receives two arguments" '[a b]])
	    

Without a doc string : (defn foo [a b c d])


		(s/conform ::defn-args ['foo '[a b c d]])
	    

A simplified version of the args of fn:

(It's not exactly a collection of symbols)


	      (s/def ::good-symbol #(and (symbol? %) (not= % '&)))
	      (s/def ::fn-args (s/and vector?
              (s/cat :args (s/* ::good-symbol)
              :rest (s/? (s/cat :& '#{&}
              :other ::good-symbol)))))
	  

	      (s/conform ::fn-args '[a b & c])
	  

s/alt vs. s/or

Usually inside the spec of a sequence, we use s/alt:


(s/def ::seq-alt (s/cat :str string?
                  :numbers-alt-string (s/alt :nums (s/* number?)
                                             :strs (s/* string?))))
(s/conform ::seq-alt ["hello" 1 2 3])
	

When we need part of the sequence to be nested, we use s/or:


(s/def ::seq-or (s/cat :str string?
                  :numbers-or-string (s/or :nums (s/* number?) 
                                           :strs (s/* string?))))
(s/conform ::seq-or ["hello" [1 2 3]])
	

One last thing...

You can describe a spec


	    (s/describe ::fn-args)
	

Interlude

What have we learned?

References

Questions so far?

Want more?

Data Generation with Clojure.spec

powered by KLIPSE /