clojure.spec
Guide中的一个示例是一个简单的选项解析规范:
(require '[clojure.spec :as s])
(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"]}]
稍后,在validation部分中,定义了一个函数,使用此规范在内部conform
输入:
(defn- set-config [prop val]
(println "set" prop val))
(defn configure [input]
(let [parsed (s/conform ::config input)]
(if (= parsed ::s/invalid)
(throw (ex-info "Invalid input" (s/explain-data ::config input)))
(doseq [{prop :prop [_ val] :val} parsed]
(set-config (subs prop 1) val)))))
(configure ["-server" "foo" "-verbose" true "-user" "joe"])
;; set server foo
;; set verbose true
;; set user joe
;;=> nil
由于指南很容易从REPL中获取,因此所有这些代码都在同一名称空间中进行评估。但是,在this answer中,@ levand建议将规范放在不同的命名空间中:
我通常将规范放在他们自己的命名空间中,以及他们描述的命名空间。
这会打破上面::config
的使用,但这个问题可以解决:
规范密钥名称最好位于代码的名称空间中,但不是规范的名称空间。通过在关键字上使用命名空间别名,这仍然很容易做到:
(ns my.app.foo.specs (:require [my.app.foo :as f])) (s/def ::f/name string?)
他继续解释规范和实现可以放在同一个命名空间中,但它不是理想的:
虽然我当然可以将它们放在同一文件中的规范代码旁边,但这会损害IMO的可读性。
但是,我很难看到这对destructuring有什么用处。作为一个例子,我将一个小Boot项目放在一起,上面的代码被翻译成多个名称空间。
boot.properties
:
BOOT_CLOJURE_VERSION=1.9.0-alpha7
src/example/core.clj
:
(ns example.core
(:require [clojure.spec :as s]))
(defn- set-config [prop val]
(println "set" prop val))
(defn configure [input]
(let [parsed (s/conform ::config input)]
(if (= parsed ::s/invalid)
(throw (ex-info "Invalid input" (s/explain-data ::config input)))
(doseq [{prop :prop [_ val] :val} parsed]
(set-config (subs prop 1) val)))))
src/example/spec.clj
:
(ns example.spec
(:require [clojure.spec :as s]
[example.core :as core]))
(s/def ::core/config
(s/* (s/cat :prop string?
:val (s/alt :s string? :b boolean?))))
build.boot
:
(set-env! :source-paths #{"src"})
(require '[example.core :as core])
(deftask run []
(with-pass-thru _
(core/configure ["-server" "foo" "-verbose" true "-user" "joe"])))
但是当然,当我实际运行时,我收到一个错误:
$ boot run
clojure.lang.ExceptionInfo: Unable to resolve spec: :example.core/config
我可以通过将(require 'example.spec)
添加到build.boot
来解决此问题,但这很难看并且容易出错,只会随着我的规范命名空间数量的增加而变得更加严重。出于几个原因,我不能require
来自实现名称空间的spec命名空间。以下是使用fdef
的示例。
boot.properties
:
BOOT_CLOJURE_VERSION=1.9.0-alpha7
src/example/spec.clj
:
(ns example.spec
(:require [clojure.spec :as s]))
(alias 'core 'example.core)
(s/fdef core/divisible?
:args (s/cat :x integer? :y (s/and integer? (complement zero?)))
:ret boolean?)
(s/fdef core/prime?
:args (s/cat :x integer?)
:ret boolean?)
(s/fdef core/factor
:args (s/cat :x (s/and integer? pos?))
:ret (s/map-of (s/and integer? core/prime?) (s/and integer? pos?))
:fn #(== (-> % :args :x) (apply * (for [[a b] (:ret %)] (Math/pow a b)))))
src/example/core.clj
:
(ns example.core
(:require [example.spec]))
(defn divisible? [x y]
(zero? (rem x y)))
(defn prime? [x]
(and (< 1 x)
(not-any? (partial divisible? x)
(range 2 (inc (Math/floor (Math/sqrt x)))))))
(defn factor [x]
(loop [x x y 2 factors {}]
(let [add #(update factors % (fnil inc 0))]
(cond
(< x 2) factors
(< x (* y y)) (add x)
(divisible? x y) (recur (/ x y) y (add y))
:else (recur x (inc y) factors)))))
build.boot
:
(set-env!
:source-paths #{"src"}
:dependencies '[[org.clojure/test.check "0.9.0" :scope "test"]])
(require '[clojure.spec.test :as stest]
'[example.core :as core])
(deftask run []
(with-pass-thru _
(prn (stest/run-all-tests))))
第一个问题最明显:
$ boot run
clojure.lang.ExceptionInfo: No such var: core/prime?
data: {:file "example/spec.clj", :line 16}
java.lang.RuntimeException: No such var: core/prime?
在factor
的规范中,我想使用我的prime?
谓词验证返回的因子。关于这个factor
规范的一个很酷的事情是,假设prime?
是正确的,它既完全记录factor
函数,也不需要我为该函数编写任何其他测试。但如果您认为这太酷了,可以用pos?
或其他东西替换它。
但不出所料,当您再次尝试boot run
时仍会出现错误,这次抱怨:args
或#'example.core/divisible?
或#'example.core/prime?
规范{1}}(无论哪个首先尝试)都会丢失。这是因为,无论您是否alias
命名空间,#'example.core/factor
都不会使用该别名,除非您给它的符号命名为已经存在。如果var不存在,则符号不会扩展。 (为了更加有趣,请从fdef
中移除:as core
并查看会发生什么。)
如果您想保留该别名,则需要从build.boot
移除(:require [example.spec])
并向example.core
添加(require 'example.spec)
。当然,build.boot
需要在之后来require
,否则它将无效。那么,为什么不直接将example.core
放入require
?
所有这些问题都可以通过将规范放在与实现相同的文件中来解决。那么,我真的应该将规范放在与实现不同的命名空间中吗?如果是这样,我上面详述的问题如何解决?
答案 0 :(得分:8)
此问题说明了在 应用程序中使用的规范与用于测试应用程序的规范之间的重要区别。
应用程序中用于符合或验证输入的规范 - 如此处:example.core/config
- 是应用程序代码的一部分。它们可能位于使用它们的同一文件中,也可能位于单独的文件中。在后一种情况下,应用程序代码必须:require
规范,就像任何其他代码一样。
用作测试的规范在 >>指定的代码后加载。这些是您的fdef
和生成器。您可以将这些命名空间与代码放在一个单独的命名空间中 - 即使是在一个单独的目录中,也不会与您的应用程序打包在一起 - 它们将:require
代码。
您可能有两种规格使用的谓词或效用函数。这些将在他们自己的单独命名空间中。