1. 사전 예비 지식
1.1. spec은 predicate으로 이루어져 있다
1.2. unnamespaced/namespaced keyword
user> :cat ; unnamespaced keyword
:cat
user> :animal/cat ; namespaced keyword
:animal/cat
user> ::cat ; namespaced keyword
:user/cat
user> (require '[very.very.long.namespace :as long])
user> ::long/cat ; alias substitution
:very.very.long.namespace/cat
1.3. set 자료형은 함수명 자리에 올 수 있다
user> #{10 20 30 40}
#{20 40 30 10}
user> (#{10 20 30 40} 10)
10
user> (#{10 20 30 40} 50)
nil
2. clojure.spec API
2.1. s/valid?
(valid? spec value) => boolean spec ::= predicate | namespaced-keyword
(s/valid? even? 10) ; => true
(s/valid? string? "abc") ; => true
(s/valid? #(> % 5) 10) ; => true
(s/valid? #(> % 5) 0) ; => false
(s/valid? #{10 20 30 40} 10) ; => true
(s/valid? #{10 20 30 40} 50) ; => false
2.2. s/def
def
는 spec을 정의하고, 중앙 저장소에 이를 저장한다. 이렇게 spec을 global하게 저장하는
이유는 재사용도를 증가시키기 위해서다.
(def namespaced-keyword spec) => namespaced-keyword spec ::= predicate | namespaced-keyword
(s/def ::suit #{:club :diamond :heart :spade})
; => :spec-guide.api/suit
(s/valid? ::suit ::space)
; => false
2.3. s/conform
(conform spec value) spec ::= predicate | namespaced-keyword
(s/conform ::suit :club) ; => :club
(s/conform ::suit :hello) ; => :clojure.spec/invalid
2.4. spec의 합성
2.4.1. s/and
(and spec+) => spec spec ::= predicate | namespaced-keyword
(s/def ::big-even (s/and int? even? #(> % 1000)))
(s/valid? ::big-even :foo) ; => false
(s/valid? ::big-even 10) ; => false
(s/valid? ::big-even 100000) ; => true
2.4.2. s/or
(or <tag spec>+) => spec tag ::= keyowrd spec ::= predicate | namespaced-keyword
or
의 경우에는 spec 앞에 tag를 붙여 주어야 한다. 아래에 소개하는 cat
도
마찬가지이다.
여러가지 경우의 수로 분기하는 경우에는 tag를 붙여 주는데, 나중에 분기되는 항목 중의 어느 항목에서 문제가 발생했는지 식별하기 위한 용도로 사용된다.
(s/def ::name-or-id (s/or :name string?
:id int?))
(s/valid? ::name-or-id "abc") ; => true
(s/valid? ::name-or-id 100) ; => true
(s/valid? ::name-or-id :foo) ; => false
(s/conform ::name-or-id "abc") ; => [:name "abc"]
(s/conform ::name-or-id 100) ; => [:id 100]
(s/conform ::name-or-id :foo) ; => :clojure.spec/invalid
2.5. s/explain
(explain spec value) => nil spec ::= predicate | namespaced-keyword
explain
은 spec을 통과하지 못한 이유를 설명해 준다.
(when (= (s/conform ::name-or-id :foo)
:clojure.spec/invalid)
(s/explain ::name-or-id :foo))
; >> val: :foo fails
; spec: :spec-guide.api/name-or-id
; at: [:name] predicate: string?
;
; val: :foo fails
; spec: :spec-guide.api/name-or-id
; at: [:id] predicate: int?
;
; => nil
(s/explain ::name-or-id "tom")
; >> Success!
; => nil
-
s/explain
: 결과를 stdout에 출력한다. -
s/explain-str
: 결과를 문자열로 반환한다. -
s/explain-data
: 결과를 클로저 데이터형으로 반환한다.
(s/explain-str ::name-or-id :foo)
; => "val: :foo fails spec: :spec-guide.api/name-or-id at: [:name] predicate: string?\nval: :foo fails spec: :spec-guide.api/name-or-id at: [:id] predicate: int?\n"
(s/explain-data ::name-or-id :foo)
; => #:clojure.spec{:problems ({:path [:name],
; :pred string?,
; :val :foo,
; :via [:spec-guide.api/name-or-id],
; :in []}
; {:path [:id],
; :pred int?,
; :val :foo,
; :via [:spec-guide.api/name-or-id],
; :in []})}
2.6. s/keys: map 자료형의 spec 정의
(keys < keyword [namespacd-key+] >+) => spec keyword ::= :req | :opt | :req-un | :opt-un
2.6.1. namespaced keys
(s/def ::first-name string?)
(s/def ::last-name string?)
(s/def ::age int?)
(s/def ::person (s/keys :req [::first-name ::last-name]
:opt [::age]))
(s/valid? ::person
{::first-name "Elon"
::last-name "Musk"
::age 45})
; => true
(s/conform ::person
{::first-name "Elon"
::last-name "Musk"})
; => #:spec-guide.api{:first-name "Elon", :last-name "Musk"}
(s/explain ::person
{::first-name "Elon"})
; >> val: #:spec-guide.api{:first-name "Elon"} fails
; spec: :spec-guide.api/person
; predicate: (contains? % :spec-guide.api/last-name)
2.6.2. unnamespaced keys
(s/def :unq/person
(s/keys :req-un [::first-name ::last-name]
:opt-un [::age]))
(s/conform :unq/person
{:first-name "Elon"
:last-name "Musk"})
; => {:first-name "Elon", :last-name "Musk"}
(s/explain :unq/person
{:first-name "Elon" :age "45"})
; >> val: {:first-name "Elon", :sex :mail} fails
; spec: :unq/person
; predicate: (contains? % :last-name)
;
; In: [:age]
; val: "45" fails
; spec: :spec-guide.api/age
; at: [:age]
; predicate: int?
2.6.3. unnamespaced keys and defrecord
(defrecord Person [first-name last-name age]) (s/conform :unq/person (->Person "Elon" "Musk" 45)) ; => #spec_guide.api.Person{:first-name "Elon", :last-name "Musk", :age 45} (s/explain :unq/person (->Person "Elon" nil nil)) ; >> In: [:last-name] ; val: nil fails ; spec: :spec-guide.api/last-name ; at: [:last-name] ; predicate: string? ; ; In: [:age] ; val: nilfails ; spec: :spec-guide.api/age ; at: [:age] ; predicate: int?
2.7. s/keys*: keyword arguments spec 정의
(keys* < keyword [namespacd-key+] >+) => spec keyword ::= :req | :opt | :req-un | :opt-un
(s/def ::port number?)
(s/def ::host string?)
(s/def ::id keyword?)
(s/def ::server (s/keys* :req [::id ::host] :opt [::port]))
(s/conform ::server [::id :s1 ::host "example.com" ::port 5555])
2.8. s/merge: spec의 병합
(merge keys-spec+) => spec keys-spec ::= spec created by s/keys
;; :animal spec
(s/def :animal/kind string?)
(s/def :animal/says string?)
(s/def :animal/common (s/keys :req [:animal/kind :animal/says]))
;; :dog spec
(s/def :dog/tail? boolean?)
(s/def :dog/breed string?)
;; merged :animal/dog spec
(s/def :animal/dog (s/merge :animal/common
(s/keys :req [:dog/tail? :dog/breed])))
(s/valid? :animal/dog
{:animal/kind "dog"
:animal/says "woof"
:dog/tail? true
:dog/breed "retriever"})
; => true
2.9. s/multi-spec
defmulti
+ defmethod
+ multi-spec
--> spec에 다형성(polymorphism)을 도입한 것.
spec 정의
;; common spec
(s/def :event/type keyword?)
(s/def :event/timestamp int?)
;; only for :event/search spec
(s/def :search/url string?)
;; only for :event/error spec
(s/def :error/message string?)
(s/def :error/code int?)
defmulti + defmethod 정의
(defmulti event-type :event/type)
(defmethod event-type :event/search [_]
(s/keys :req [:event/type :event/timestamp :search/url]))
(defmethod event-type :event/error [_]
(s/keys :req [:event/type :event/timestamp :error/message :error/code]))
multi-spec 정의
(s/def :event/event (s/multi-spec event-type :event/type))
실행
(s/valid? :event/event
{:event/type :event/search
:event/timestamp 1463970123000
:search/url "http://clojure.org"})
; => true
(s/valid? :event/event
{:event/type :event/error
:event/timestamp 1463970123000
:error/message "Invalid host"
:error/code 500})
; => true
(s/explain :event/event
{:event/type :event/restart})
; >> val: #:event{:type :event/restart} fails
; spec: :event/event
; at: [:event/restart]
; predicate: event-type, no method
(s/explain :event/event
{:event/type :event/search
:search/url 200})
; >> val: {:event/type :event/search, :search/url 200} fails
; spec: :event/event
; at: [:event/search]
; predicate: (contains? % :event/timestamp)
;
; In: [:search/url]
; val: 200 fails
; spec: :search/url
; at: [:event/search :search/url]
; predicate: string?
2.10. Collections
coll-of map-of tuple ----------------------------------- list O X X vector O X O map O O X set O X X 요소 타입 동일 동일 이질 크기 임의 임의 고정
2.10.1. s/coll-of
(s/conform (s/coll-of keyword?) [:a :b :c])
; => [:a :b :c]
(s/conform (s/coll-of number?) #{5 10 2})
; => #{2 5 10}
(s/def ::vnum3 (s/coll-of number? :kind vector? :count 3 :distinct true :into #{}))
(s/conform ::vnum3 [1 2 3])
; => #{1 2 3}
(s/explain ::vnum3 #{1 2 3}) ;; not a vector
; >> val: #{1 3 2} fails
; spec: ::vnum3
; predicate: clojure.core/vector?
(s/explain ::vnum3 [1 1 1]) ;; not distinct
; >> val: [1 1 1] fails
; spec: ::vnum3
; predicate: distinct?
(s/explain ::vnum3 [1 2 :a]) ;; not a number
; >> In: [2]
; val: :a fails
; spec: ::vnum3
; predicate: number?
2.10.2. s/map-of
(s/def ::scores (s/map-of string? int?))
(s/conform ::scores {"Sally" 1000, "Joe" 500})
; => {"Sally" 1000, "Joe" 500}
2.10.3. s/tuple
(s/def ::point (s/tuple double? double? double?))
(s/conform ::point [1.5 2.5 -0.5])
; => [1.5 2.5 -0.5]
(s/explain ::point [1.5 2.5 5])
; >> In: [2]
; val: 5 fails
; spec: :spec-guide.api/point
; at: [2]
; predicate: double?
;; tuple: list 자료형을 대상으로는 작동하지 않는다.
;; 대상 자료형이 반드시 vector 형이어야 한다.
(s/conform ::point '(1.5 2.5 -0.5))
; => :clojure.spec/invalid
2.11. Sequences: Sequentials (vector와 list) 대상
2.11.1. regular expression 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 -
&
- regex operators with filters
s/cat
과 s/alt
는 s/or
와 마찬가지로 <keyword spec> 쌍으로 이루어지는 인수를 가진다.
2.11.2. s/cat
(cat <keyword spec>+)
(s/def ::ingredient (s/cat :quantity number? :unit keyword?))
(s/conform ::ingredient [2 :teaspoon])
; => {:quantity 2, :unit :teaspoon}
(s/conform ::ingredient '(2 :teaspoon))
; => {:quantity 2, :unit :teaspoon}
;; pass string for unit instead of keyword
(s/explain ::ingredient [11 "peaches"])
; >> In: [1]
; val: "peaches" fails
; spec: :spec-guide.api/ingredient
; at: [:unit]
; predicate: keyword?
;; leave out the unit
(s/explain ::ingredient [2])
; >> val: () fails
; spec: :spec-guide.api/ingredient
; at: [:unit]
; predicate: keyword?, Insufficient input
2.11.3. s/* s/+ s/?
(* spec) (+ spec) (? spwc)
(s/def ::seq-of-keywords (s/* keyword?))
(s/conform ::seq-of-keywords [:a :b :c])
; => [:a :b :c]
(s/explain ::seq-of-keywords [10 20])
; >> In: [0]
; val: 10 fails
; spec: :spec.examples.guide/seq-of-keywords
; predicate: keyword?
(s/def ::odds-then-maybe-even (s/cat :odds (s/+ odd?)
:even (s/? even?)))
(s/conform ::odds-then-maybe-even [1 3 5 100])
; => {:odds [1 3 5], :even 100}
(s/conform ::odds-then-maybe-even [1])
; => {:odds [1]}
(s/explain ::odds-then-maybe-even [100])
; >> In: [0]
; val: 100 fails
; spec: ::odds-then-maybe-even
; at: [:odds]
; predicate: odd?
;; opts are alternating keywords and booleans
(s/def ::opts (s/* (s/cat :opt keyword? :val boolean?)))
(s/conform ::opts [:silent? false :verbose true])
; => [{:opt :silent?, :val false} {:opt :verbose, :val true}]
2.11.4. s/alt
(alt <keyword spec>+) => spec
(s/def ::config (s/*
(s/cat :prop string?
:val (s/alt :s string? :b boolean?))))
(s/conform ::config ["-server" "foo" "-verbose" true "-user" "joe"])
; => [{:prop "-server", :val [:s "foo"]}
; {:prop "-verbose", :val [:b true]}
; {:prop "-user", :val [:s "joe"]}]
2.11.5. s/&
(& regex-operator spec+) => spec
(s/def ::even-strings (s/& (s/* string?) #(even? (count %))))
(s/valid? ::even-strings ["a"]) ; => false
(s/valid? ::even-strings ["a" "b"]) ; => true
2.11.6. s/describe
(describe spec) => list
(s/describe ::seq-of-keywords)
; => (* keyword?)
(s/describe ::odds-then-maybe-even)
; => (cat :odds (+ odd?) :even (? even?))
(s/describe ::opts)
; => (* (cat :opt keyword? :val boolean?))
2.11.7. s/spec
nested sequential collection
(spec form) form ::= predicate | regex-operator
(s/def ::nested
(s/cat :names-kw #{:names}
:names (s/spec (s/* string?))
:nums-kw #{:nums}
:nums (s/spec (s/* number?))))
(s/conform ::nested [:names ["a" "b"] :nums [1 2 3]])
; => {:names-kw :names, :names ["a" "b"], :nums-kw :nums, :nums [1 2 3]}
(s/def ::unnested
(s/cat :names-kw #{:names}
:names (s/* string?)
:nums-kw #{:nums}
:nums (s/* number?)))
(s/conform ::unnested [:names "a" "b" :nums 1 2 3])
; => {:names-kw :names, :names ["a" "b"], :nums-kw :nums, :nums [1 2 3]}
2.12. s/fedf: function spec
(defn ranged-rand
"Returns random int in range start <= rand < end"
[start end]
(+ start (long (rand (- end start)))))
(s/fdef ranged-rand
:args (s/and (s/cat :start int? :end int?)
#(< (:start %) (:end %)))
:ret int?
:fn (s/and #(>= (:ret %) (-> % :args :start))
#(< (:ret %) (-> % :args :end))))
(ranged-rand 5 10)
; => 7
(ranged-rand 10 5)
; => 9
(stest/instrument `ranged-rand)
(ranged-rand 5 10)
; => 7
; (ranged-rand 10 5)
; >> Call to #'spec-guide.api/ranged-rand did not conform to spec:
; val: {:start 10, :end 5} fails
; at: [:args]
; predicate: (< (:start %) (:end %))
; :clojure.spec/args (10 5)
; :clojure.spec/failure :instrument
; :clojure.spec.test/caller {:file "form-init7709795464976482689.clj",
; :line 400,
; :var-scope spec-guide.api/eval13655}
2.13. s/fdef: macro spec
(s/fdef clojure.core/declare
:args (s/cat :names (s/* simple-symbol?))
:ret any?)
(declare 100)
; >> Unhandled clojure.lang.ExceptionInfo
; Call to clojure.core/declare did not conform to spec:
; In: [0]
; val: 100 fails
; at: [:args :names]
; predicate: simple-symbol?
; :clojure.spec/args (100)
2.14. s/fspec: anonymous function spec
(defn adder [x] #(+ x %))
(s/fdef adder
:args (s/cat :x number?)
:ret (s/fspec :args (s/cat :y number?)
:ret number?)
:fn #(= (-> % :args :x) ((:ret %) 0)))
3. test case 생성
3.1. s/gen, gen/generate, gen/sample
(gen spec) => generator (generate generator) => 한 개의 sample (sample generator) => 10개(default)의 sample (sample generator n) => n개의 sample
(gen/generate (s/gen int?))
; => -959
(gen/generate (s/gen nil?))
; => nil
(gen/sample (s/gen string?))
; => ("" "" "" "" "8" "W" "" "G74SmCm" "K9sL9" "82vC")
(gen/sample (s/gen #{:club :diamond :heart :spade}))
; => (:heart :diamond :heart :heart :heart :diamond :spade :spade :spade :club)
(gen/sample (s/gen (s/cat :k keyword? :ns (s/+ number?))))
; => ((:D -2.0)
(:q4/c 0.75 -1)
(:*!3/? 0)
(:+k_?.p*K.*o!d/*V -3)
(:i -1 -1 0.5 -0.5 -4)
(:?!/! 0.515625 -15 -8 0.5 0 0.75)
(:vv_z2.A??!377.+z1*gR.D9+G.l9+.t9/L34p -1.4375 -29 0.75 -1.25)
(:-.!pm8bS_+.Z2qB5cd.p.JI0?_2m.S8l.a_Xtu/+OM_34* -2.3125)
(:Ci 6.0 -30 -3 1.0)
(:s?cw*8.t+G.OS.xh_z2!.cF-b!PAQ_.E98H4_4lSo/?_m0T*7i
4.4375 -3.5 6.0 108 0.33203125 2 8 -0.517578125 -4))
3.1.1. s/exercise
(exercise generator) => 10개의 [sample conformed-value] (exercise generator n) => n개의 [sample conformed-value]
(s/exercise (s/cat :k keyword? :ns (s/+ number?)) 5)
; => ([(:y -2.0) {:k :y, :ns [-2.0]}]
; [(:_/? -1.0 0.5) {:k :_/?, :ns [-1.0 0.5]}]
; [(:-B 0 3.0) {:k :-B, :ns [0 3.0]}]
; [(:-!.gD*/W+ -3 3.0 3.75) {:k :-!.gD*/W+, :ns [-3 3.0 3.75]}]
; [(:_Y*+._?q-H/-3* 0 1.25 1.5) {:k :_Y*+._?q-H/-3*, :ns [0 1.25 1.5]}])
(s/exercise (s/or :k keyword? :s string? :n number?) 5)
; => ([:H [:k :H]]
; [:ka [:k :ka]]
; [-1 [:n -1]]
; ["" [:s ""]]
; [-3.0 [:n -3.0]])
3.1.2. s/exercise-fn
(exercise-fn symbol) => 10개의 [sample conformed-value] (exercise-fn symbol n) => n개의 [sample conformed-value]
(s/exercise-fn `ranged-rand 5)
; => ([(-2 -1) -2]
; [(-3 3) 0]
; [(0 1) 0]
; [(-8 -7) -8]
; [(3 13) 7]