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部分中,定义了一个函数,该函数使用此spec在内部 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的用法,但是可以解决该问题:

他继续解释说,规范和实现可以放在同一个 namespace 中,但这并不理想:

但是,我在查看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来解决此问题,但这很丑陋且容易出错,并且随着我的规范 namespace 数量的增加,它只会变得越来越多。由于多种原因,我无法从实现 namespace require规范 namespace 。这是使用 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?(无论哪种情况首先尝试)的#'example.core/factor规范都丢失了。这是因为,不管您是否使用 alias 一个命名空间,fdef都不会使用该别名,除非您为其赋予的符号命名一个已经存在的变量。如果var不存在,则该符号不会扩展。 (为获得更多乐趣,请从:as core中删除build.boot并查看会发生什么。)
如果要保留该别名,则需要从(:require [example.spec])中删除example.core,并在(require 'example.spec)中添加一个build.boot。当然,该require必须在example.core的后面,否则它将不起作用。到那时,为什么不将require直接放入example.spec呢?
所有这些问题都可以通过将规范与实现文件放在同一文件中来解决。那么,我真的应该将规范放在与实现分开的命名空间中吗?如果是这样,如何解决我上面详述的问题?

最佳答案

这个问题说明了在应用程序中使用的规范与用于测试应用程序的规范之间的重要区别。

应用程序中用于规范或验证输入的规范(如此处的:example.core/config)是应用程序代码的一部分。它们可能位于使用它们的同一文件中,也可能位于单独的文件中。在后一种情况下,应用程序代码必须与规范一样:require,就像其他任何代码一样。

用作测试的规范会在指定的代码之后加载。这些是您的fdef和生成器。您可以将它们放在代码的单独命名空间中,甚至放在应用程序未打包的单独目录中,它们也会:require代码。

这两种规范都有可能使用某些谓词或实用程序函数。它们将全部放在一个单独的命名空间中。

关于clojure - 如果规范位于单独的命名空间中,如何将其用于预期目的?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/38024650/

10-14 07:31