如果它们位于单独的命名空间中,我如何将我的规范用于其预期目的?

Posted

技术标签:

【中文标题】如果它们位于单独的命名空间中,我如何将我的规范用于其预期目的?【英文标题】:How can I use my specs for their intended purposes if they are in a separate namespace? 【发布时间】:2016-10-27 17:42:03 【问题描述】:

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 部分中,定义了一个函数,在内部 conforms 使用此规范输入其输入:

(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 实现命名空间中的规范命名空间。这是一个使用 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 不存在,则符号不会被扩展。 (为了更有趣,从build.boot 中删除:as core 看看会发生什么。)

如果您想保留该别名,您需要从example.core 中删除(:require [example.spec]),并将(require 'example.spec) 添加到build.boot。当然,require 需要example.core 之后出现,否则它将不起作用。到那时,为什么不直接将require 放入example.spec

所有这些问题都可以通过将规范与实现放在同一个文件中来解决。那么,我真的应该将规范放在与实现不同的命名空间中吗?如果是这样,我上面详述的问题如何解决?

【问题讨论】:

您很好地说明了为什么在使用解构时最好将规范放在同一个命名空间中。似乎不可能避免以混乱的代码为代价获得更精确的界面,但如果有的话那就太好了......所以我希望有人能回答这个问题:) 我相信预期的做法是在example.core 中要求example.spec,在example.spec 中只要求alias example.core 而不是要求它... @LeonGrapenthin 那行不通;查看我的最新编辑。 @SamEstep 您可以尝试将完全限定的 ns 用于您的 fdef,而根本不需要或别名 example.core - 或者有人可能会争辩说,如果您的代码依赖于规范的解析器,规范成为代码的产物,因此应该直接进入代码。 @SamEstep 我真的不明白为什么您和 levand 想要将规范放入单独的 ns 中。在他的回答中,他没有争论为什么应该这样做,而且我越看你在这里阐述的问题,我相信如果没有一些新功能,他的回答就不会成立。 【参考方案1】:

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

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

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

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

【讨论】:

以上是关于如果它们位于单独的命名空间中,我如何将我的规范用于其预期目的?的主要内容,如果未能解决你的问题,请参考以下文章

如何从命名空间导出,访问默认值? [复制]

具有动态类名的 PHP 命名空间

单独的 xslt-replace 命名空间 uri

在命名空间内设计

结构成员标识符放在啥范围内?

如何在(子)模块中使用 __init__.py 来定义命名空间?