1. clojure.spec 소개
-
clojure.spec은 클로저 언어의 창시자인 Rich Hickey가 만든 라이브러리로,
clojure 1.9.0
버전에 추가될 예정이다. -
이 발표에 사용한 clojure.spec 버전은
clojure 1.9.0-alpha14
이다. -
Dynamic type/value Checking: spec을 정의하면, runtime에 type과 value를 검사할 수 있다.
-
Generative Testing: spec을 정의하면, 테스트 케이스를 자동 생성하고, 자동으로 테스트를 수행하게 할 수 있다.
-
이와같은 기능을 특정 이름공간(namespace) 또는 특정 함수만을 대상으로 켜거나 끌 수 있다. 그래서 개발/테스트 기간에만 이 기능을 작동시키고, 제품이 정식으로 출시될 때 중단시키면, 프로그램의 실행 속도에 전혀 영향을 미치지 않는다.
-
결과적으로, 동적 언어인 클로저에서 정적 언어의 특징들을 이용할 수 있게 해준다.
2. 기존의 Clojure 타입 체킹 라이브러리와의 비교
type checking run-time generative ClojureScript value checking testing 지원 ------------------------------------------------------------------------------ core.typed compile-time X X X schema run-time X △ O core.spec run-time O O O
2.1. schema와 clojure.spec 예제 비교
(ns schema-examples
(:require [schema.core :as s]))
(def Data
"A schema for a nested data type"
{:a {:b s/Str
:c s/Int}
:d [{:e s/Keyword
:f [s/Num]}]})
(s/validate
Data
{:a {:b "abc"
:c 123}
:d [{:e :bc
:f [12.2 13 100]}
{:e :bc
:f [-1]}]})
;; Success!
(s/validate
Data
{:a {:b 123
:c "ABC"}})
;; Exception -- Value does not match schema:
;; {:a {:b (not (instance? java.lang.String 123)),
;; :c (not (integer? "ABC"))},
;; :d missing-required-key}
(s/def ::b string?)
(s/def ::c int?)
(s/def ::a (s/keys :req [::b ::c]))
(s/def ::e keyword?)
(s/def ::f (s/coll-of number?))
(s/def ::d (s/coll-of (s/keys :req [::e ::f])))
(s/def ::data (s/keys :req [::a ::d]))
(s/valid? ::data {::a {::b "abc"
::c 123}
::d [{::e :bc
::f [12.2 13 100]}
{::e :bc
::f [-1] }]})
; => true
(s/valid? ::data {::a {::b 123
::c "ABC"}})
; => false
(s/explain ::data {::a {::b 123
::c "ABC"}})
; >> val: {:a {:b 123, :c "ABC"}} fails
; spec: ::data
; predicate: (contains? % ::d)
;
; In: [::a ::b]
; val: 123 fails
; spec: ::b
; at: [::a ::b]
; predicate: string?
;
; In: [::a ::c]
; val: "ABC" fails
; spec: ::c
; at: [::a ::c]
; predicate: int?
3. clojure.spec의 용도
3.1. Documentation
-
spec은 함수 입출력 데이터의 구조를 명시적으로 정의함으로써, 표준화된 documentation을 제공한다. 팀원들 사이에 그리고 자기 자신의, 코드에 대한 이해도를 높일 수 있다.
(defn snake-case
"Converts lisp-case keyword to snake-case string.
<k keyword>
<return string>
ex) :get-sock-address => \"get_sock_address\""
[k]
(-> (name k) (str/replace "-" "_")))
(s/fdef snake-case
:args (s/cat keyword?)
:ret string?)
(defn snake-case
"Converts lisp-case keyword to snake-case string.
ex) :get-sock-address => \"get_sock_address\""
[k]
(-> (name k) (str/replace "-" "_")))
3.2. Debugging
-
spec은 runtime에 데이터의 유효성(validation), 즉 타입 및 값을 검증할 수 있게 해주므로, 버그 발생 가능성을 현저히 줄일 수 있다.
3.2.1. 문제 없는 예
(max 1 2 3 4)
; => 4
(max)
; >> 1. Caused by clojure.lang.ArityException
; Wrong number of args (0) passed to: core/max
;
; AFn.java: 429 clojure.lang.AFn/throwArity
; RestFn.java: 399 clojure.lang.RestFn/invoke
; intro.clj: 14 spec-guide.intro/eval13365
; intro.clj: 14 spec-guide.intro/eval13365
; Compiler.java: 6978 clojure.lang.Compiler/eval
; Compiler.java: 7430 clojure.lang.Compiler/load
3.2.2. 문제 있는 예
(defn my-max [coll]
(apply max coll))
(my-max [1 2 3 4])
; => 4
(my-max nil)
; >> 1. Unhandled clojure.lang.ArityException
; Wrong number of args (0) passed to: core/max
;
; AFn.java: 429 clojure.lang.AFn/throwArity
; RestFn.java: 399 clojure.lang.RestFn/invoke
; AFn.java: 152 clojure.lang.AFn/applyToHelper
; RestFn.java: 132 clojure.lang.RestFn/applyTo
; core.clj: 657 clojure.core/apply
; core.clj: 652 clojure.core/apply
; REPL: 7 spec-guide.intro/my-max
; REPL: 6 spec-guide.intro/my-max
; REPL: 28 spec-guide.intro/eval10841
; REPL: 28 spec-guide.intro/eval10841
; Compiler.java: 6977 clojure.lang.Compiler/eval
; Compiler.java: 6940 clojure.lang.Compiler/eval
; core.clj: 3187 clojure.core/eval
; ......
3.2.3. core.spec으로 문제 해결
(ns spec-guide.intro
(:require [clojure.spec :as s]
[clojure.spec.gen :as gen]
[clojure.spec.test :as stest]))
(s/fdef my-max2
:args (s/cat :coll (s/coll-of number?))
:ret number?)
(defn my-max2 [coll]
(apply max coll))
(stest/instrument `my-max2)
(my-max2 [1 2 3 4])
; => 4
(my-max2 nil)
; >> 1. Unhandled clojure.lang.ExceptionInfo
; Call to spec-guide.intro/my-max2 did not conform to spec:
; In: [0]
; val: nil fails
; at: [:args :coll]
; predicate: coll?
; :clojure.spec/args (nil)
; :clojure.spec/failure :instrument
; :clojure.spec.test/caller {:file "form-init414233231437328049.clj",
; :line 63, :var-scope spec-guide.intro/eval10997}
;
; {:clojure.spec/problems [{:path [:args :coll],
; :pred coll?,
; :val nil,
; :via [],
; :in [0]}],
; :clojure.spec/args (nil),
; :clojure.spec/failure :instrument,
; :clojure.spec.test/caller {:file "form-init414233231437328049.clj",
; :line 63,
; :var-scope spec-guide.intro/eval10997}}
3.2.4. core.spec은 실행 중 값도 검사할 수 있다
(s/fdef my-max3
:args (s/and (s/cat :coll (s/coll-of number?))
#(every? (fn [num]
(< num 10))
(:coll %) ))
:ret number?)
(defn my-max3 [coll]
(apply max coll))
(stest/instrument `my-max3)
(my-max3 [1 2 3 14])
; >> 1. Unhandled clojure.lang.ExceptionInfo
; Call to #spec-guide.intro/my-max3# did not conform to spec:
; val: {:coll [1 2 3 14]} fails
; at: [:args]
; predicate: (every? (fn [num] (< num 10)) (:coll %))
; :clojure.spec/args ([1 2 3 14])
; :clojure.spec/failure :instrument
; :clojure.spec.test/caller {:file "form-init414233231437328049.clj",
; :line 97,
; :var-scope spec-guide.intro/eval11148}
3.3. Generative Testing
-
spec은 자동 테스트 케이스 생성(generative testing) 및 테스팅 기능을 제공함으로써 코드의 무결성을 높일 수 있다.
(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))))
(defn ranged-rand
"Returns random int in range start <= rand < end"
[start end]
(+ start (long (rand (- end start)))))
;; 자동 샘플 생성
(s/exercise-fn `ranged-rand 5)
; => ([(-2 -1) -2] [(-2 0) -1] [(-2 0) -2] [(0 2) 1] [(-14 1) -3])
;; 자동 테스트 수행
(stest/check `ranged-rand)
; => ({:spec #object[clojure.spec$fspec_impl$reify__14282 0x28315748
; "clojure.spec$fspec_impl$reify__14282@28315748"],
; :clojure.spec.test.check/ret {:result true,
; :num-tests 1000,
; :seed 1478747287406},
; :sym spec-guide.api/ranged-rand})
instrument check --------------------------------- :args O O :ret X O :fn X O
3.4. Destructuring(구조분해)
-
spec은 데이터의 구조분해(일종의 코드 parsing) 기능을 제공한다. 이 기능이 매크로와 결합되면, 기존에 Clojure에서 불가능하지는 않지만 하기 어려웠던 일을 쉽게 할 수 있다. (참고: Custom defn macro with clojure.spec)
original code | target code |
---|---|
|
|
(defn prepend-log [name body]
(cons `(println ~name "has been called.") body))
(defn update-conf [{:keys [:bs] :as conf} body-update-fn]
(update-in conf [:bs 1 :body] body-update-fn))
(defmacro defnlog [& args]
(let [{:keys [name] :as conf} (s/conform ::defn-args args)
new-conf (update-conf conf (partial prepend-log (str name)))
new-args (s/unform ::defn-args new-conf)]
(cons `defn new-args)))
(s/conform ::defn-args '(add [a b] (+ a b)))
; => {:name add,
; :bs [:arity-1 {:args {:args [[:sym a] [:sym b]]},
; :body [(+ a b)]}]}
(defnlog add [a b]
(+ a b))
(add 10 20)
; >> add has been called.
; => 30