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]