(str (js/Date.))
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)
The interactive code snippets are powered by KLIPSE. 🤗
(map inc [1 2 3])
Most significant part of Clojure 1.9
.
clojure.spec
features:
All you have to do is to require clojure.spec
like this:
(ns foo.core
(:require [clojure.spec.alpha :as s]))
(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))))
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!
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?
Explain validation
(s/explain-str ::my-big-integer "abc")
Not so useful 😕
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!")
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!)
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")
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
?
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!
You can desribe the shape of a data sequence using Regular Expressions operators:
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))
You can describe the shape of a data sequence using Regular Expressions operators:
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]])
You can describe a spec
(s/describe ::fn-args)
What have we learned?
powered by KLIPSE /