如何使Clojure的:pre和:post在失败时报告它们的值?

34
(defn string-to-string [s1] 
  {:pre  [(string? s1)]
   :post [(string? %)]}
  s1)

我喜欢使用 :pre 和 :post 条件,它们可以帮助我更快地发现自己是否把“方块钉子放入圆孔”了。也许这是错误的,但我喜欢将它们作为一种简单的类型检查器。虽然这不是哲学问题,但这是一个简单的问题。

从上面的代码看来,我应该很容易地确定 s1 是函数参数在 :pre 条件中的变量名。类似地,%:post 条件中总是函数返回值。

我的希望是,在 AssertionError 中打印出 s1% 的值,以便当相应条件失败时能够得到类似以下的输出:

(string-to-string 23)

AssertionError Assert failed: (string? s1) 
(pr-str s1) => 23 

通过AssertionError包含一个单独的行,其中列出了从函数参数列表中识别并在失败的测试中引用的每个变量。当函数的返回值未能满足:post条件时,我也希望有类似的输出。

这将使我轻松快速地发现我在从AssertionError进行诊断时如何误用函数。它至少会让我知道值是nil还是实际值(这是我常犯的错误)。

我有一些想法,可以通过宏来实现这一点,但我想知道是否有任何安全和全局的方法来重新定义(defn(fn等函数,以便:pre:post也会打印导致测试失败的值。


1
我可能对你的问题有所疏漏,但是在:pre或:post中允许使用try..catch吗?这样做不好吗? - octopusgrabbus
1
我假设任何有效的形式在 :pre 和 :post 中都可以使用(虽然不确定)。我想指出 :pre 和 :post 中的形式似乎被插入到了一个断言中。所以,你可以尝试使用 try (catch...),但那最终会变成一个 (assert (try (catch...)));虽然这样也可以(尽管有点杂乱,而且只能在实际断言之前打印),但我的目标是实际修改底层的 AssertError,并将相关符号和 (pr-str values) 添加到 AssertionError 中作为消息。 - Stephen Cagle
Clojure规范示例:https://clojure.org/guides/spec#_using_spec_for_validation - xtreak
4个回答

30

你可以使用 clojure.test 中的宏 is 包装你的谓词(predicate)。

(defn string-to-string [s1] 
  {:pre  [(is (string? s1))]
   :post [(is (string? %))]}
 s1)

然后你会得到:

(string-to-string 10)
;FAIL in clojure.lang.PersistentList$EmptyList@1 (scratch.clj:5)
;expected: (string? s1)
;actual: (not (string? 10))

4
还不错,我喜欢。这是一个合理的妥协方案,基本能够满足90%的需求。不过,我认为如果在 Clojure 1.7+ 中能够自动打印出所有立即变量的值就更好了(注意要识别引用、惰性序列以及可能存在但我没有考虑到的其他问题)。 - Stephen Cagle
2
这段代码有些笨拙,无论是编写还是输出结果都不够优雅。我认为Clojure核心本身可以将其简化。也许clojure.spec:pre更加优美。 - matanster
我只是得到了 Assert failed: (is (not (neg? %)))。我错过了什么? - Hindol

14

@octopusgrabbus提出了(try...(catch...))的建议,你提到这可能会太吵闹,并且仍然被包含在一个断言中。一个更简单、噪音更少的变体是一个简单的(or (condition-here) (throw-exception-with-custom-message))语法,像这样:

(或者 (这里是条件) (抛出自定义消息的异常))
(defn string-to-string [s1] 
  {:pre  [(or (string? s1)
              (throw (Exception. (format "Pre-condition failed; %s is not a string." s1))))]
   :post [(or (string? %)
              (throw (Exception. (format "Post-condition failed; %s is not a string." %))))]}
  s1)

这实际上让你可以使用带有自定义错误消息的前置条件和后置条件--前置条件和后置条件仍然像往常一样进行检查,但是在 AssertionError 发生之前会评估你的自定义异常(因此抛出)。


4

类似下面这个例子,clojure spec 在解释问题。这将抛出一个断言错误,您可以捕获它。

 (defn string-to-string [s1] 
  {:pre [ (or (s/valid?  ::ur-spec-or-predicate s1) 
              (s/explain ::ur-spec-or-predicate s1)]}
  s1)

这不总是正确的吗? - bfontaine
1
@bfontaine 不,这个代码在 s/explain 中似乎总是返回 nil(至少在 Clojure 1.10.1 中是如此)。但是你是对的,这个答案依赖于实现细节,这些细节随时可能发生变化。我建议使用一个 if 结构,虽然这可能会有点笨重。 - mzuther

4

使用Clojure spec可以对参数进行断言,如果输入无效则会抛出异常,并提供解释为什么失败的数据(必须打开断言检查):

(require '[clojure.spec.alpha :as s])

;; "By default assertion checking is off - this can be changed at the REPL
;;  with s/check-asserts or on startup by setting the system property
;;  clojure.spec.check-asserts=true"
;;
;; quoted from https://clojure.org/guides/spec#_using_spec_for_validation
(s/check-asserts true)

(defn string-to-string [s1] 
  {:pre  [(s/assert string? s1)]
   :post [(s/assert string? %)]}
  s1)

(string-to-string nil) => #error{:cause "Spec assertion failed\nnil - failed: string?\n",
                                 :data #:clojure.spec.alpha{:problems [{:path [], :pred clojure.core/string?, :val nil, :via [], :in []}],
                                                            :spec #object[clojure.core$string_QMARK___5395 0x677b8e13 "clojure.core$string_QMARK___5395@677b8e13"],
                                                            :value nil,
                                                            :failure :assertion-failed}}

异常中的[:data :value]键显示了失败的值。[:data :problems]键显示了为什么规范认为该值无效。(在此示例中,问题很直接,但当您有嵌套地图和多个规范组合在一起时,这种解释非常有用。)
一个重要的警告是,当给定有效输入时,s/assert返回该输入,但:pre:post条件检查真实性。如果您需要的验证条件认为错误值是有效的,则需要调整验证表达式,否则s/assert将成功,但:pre:post中的真实性检查将失败。
(defn string-or-nil-to-string [s1]
  {:pre [(s/assert (s/or :string string? :nil nil?) s1)]
   :post [(s/assert string? %)]}
  (str s1))

(string-or-nil-to-string nil) => AssertionError

这是我用来避免这个问题的方法:
(defn string-or-nil-to-string [s1]
  {:pre [(do (s/assert (s/or :string string? :nil nil?) s1) true)]
   :post [(s/assert string? %)]}
  (str s1))

(string-or-nil-to-string nil) => ""

编辑:启用断言检查。


你也可以在deps.edn文件中这样设置check-asserts: :aliases {:dev {:jvm-opts ["-Dclojure.spec.check-asserts=true"]}} - Allan David

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