如果我的规格在一个单独的命名空间中,我如何使用它们的预期目的?

28

clojure.spec指南中的一个例子是一个简单的选项解析规范:

(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"]}]

稍后,在验证部分,定义了一个函数,该函数内部使用此规范将其输入符合化:
(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的用法,但该问题可以得到解决:

It is preferable for spec key names to be in the namespace of the code, however, not the namespace of the spec. This is still easy to do by using a namespace alias on the keyword:

(ns my.app.foo.specs
  (:require [my.app.foo :as f]))

(s/def ::f/name string?)
他继续解释说,规范和实现可以放在同一个命名空间中,但这并不理想:
“虽然我可以将它们与规范代码放在同一个文件中,但我认为这会影响可读性。”
然而,我很难看出这如何与 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

我可以通过在build.boot中添加(require 'example.spec)来解决这个问题,但这样做既丑陋又容易出错,在我的spec命名空间数量增加时也会变得更加如此。由于多种原因,我无法从实现命名空间中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时,仍会收到错误提示,此次错误提示是关于#'example.core/divisible?#'example.core/prime?#'example.core/factor(无论它尝试哪个先)的:args规范缺失。这是因为,无论您是否别名命名空间,fdef都不会使用该别名,除非您给定的符号已经命名了一个已存在的变量。如果变量不存在,则符号不会被扩展。(更有趣的是,从build.boot中删除:as core并查看会发生什么。)
如果您想保留该别名,您需要从example.core中删除(:require [example.spec])并将(require 'example.spec)添加到build.boot中。当然,那个require需要在example.core的后面,否则它将不起作用。此时,为什么不直接将require放入example.spec中呢?
将规范放入与实现相同的文件中可以解决所有这些问题。那么,我真的应该将规范放在与实现不同的命名空间中吗?如果是这样,如何解决我上面详细描述的问题?

3
你提出了一个非常好的观点,说明了在使用解构时将规范放在相同的命名空间中为什么更可取。似乎不可能避免通过增加代码来获得更精确的接口这种权衡,但如果有一种方法能够... 所以希望有人能够回答这个问题 :) - Timothy Pratley
我认为惯例是在example.core中要求example.spec,并且只需在example.spec中使用alias来引用example.core,而不是要求它... - Leon Grapenthin
@LeonGrapenthin,那个不行;请看我的最新编辑。 - Sam Estep
2
@SamEstep 你可以尝试使用完全限定的命名空间来定义你的fdefs,而不需要使用require或alias example.core - 或者有人可能会认为,如果你的代码依赖于规范的解析器,那么规范就成为了代码的一部分,应该直接放入代码中。 - Leon Grapenthin
1
@SamEstep,我真的不明白为什么你和levand想要将规范放入单独的ns中。在他的回答中,他没有提出任何理由为什么应该这样做,而且我越看到你在这里阐述的问题,我相信如果没有一些新功能,他的回答就无法支持。 - Leon Grapenthin
显示剩余4条评论
1个回答

13

这个问题展示了应用程序内使用的规范和用于测试应用程序的规范之间的重要区别。

在应用程序中用于符合或验证输入的规范——例如这里的:example.core/config——是应用程序代码的一部分。它们可能与使用它们的文件在同一个文件中,也可能在另一个文件中。在后一种情况下,应用程序代码必须像任何其他代码一样:require这些规范。

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

有可能你有一些谓词或实用函数同时被两种规范使用。这些应该放在一个单独的命名空间中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接