21

我在最后几天深入挖掘了 Clojure 和 ClojureScript 中的clojure.spec

到目前为止,我发现在依赖特定格式数据的公共函数中:pre和公共函数中使用规范作为保护是最有用的。:post

(defn person-name [person]
  {:pre [(s/valid? ::person person)]
   :post [(s/valid? string? %)]}
  (str (::first-name person) " " (::last-name person)))

这种方法的问题是,我得到了一个java.lang.AssertionError: Assert failed: (s/valid? ::person person)没有任何关于什么不符合规范的信息。

有谁知道如何获得更好的错误信息:pre守卫:post

我知道conformand ,但这对那些或守卫explain*没有帮助。:pre:post

4

4 回答 4

12

在较新的 alpha 中,现在s/assert可以使用它来断言输入或返回值与规范匹配。如果有效,则返回原始值。如果无效,解释结果将引发断言错误。断言可以打开或关闭,甚至可以选择从编译代码中完全省略,以产生 0 生产影响。

(s/def ::first-name string?)
(s/def ::last-name string?)
(s/def ::person (s/keys :req [::first-name ::last-name]))
(defn person-name [person]
  (s/assert ::person person)
  (s/assert string? (str (::first-name person) " " (::last-name person))))

(s/check-asserts true)

(person-name 10)
=> CompilerException clojure.lang.ExceptionInfo: Spec assertion failed
val: 10 fails predicate: map?
:clojure.spec/failure  :assertion-failed
 #:clojure.spec{:problems [{:path [], :pred map?, :val 10, :via [], :in []}], :failure :assertion-failed}
于 2016-07-22T05:24:53.867 回答
6

我认为这个想法是你spec/instrument用来验证函数输入和输出,而不是前后条件。

在这篇博文的底部有一个很好的例子:http: //gigasquidsoftware.com/blog/2016/05/29/one-fish-spec-fish/。快速总结:您可以使用 :args 和 :ret 键(因此替换前置和后置条件)为函数定义规范,包括输入和返回值,使用spec/fdef, 检测它,并且您获得类似于使用explain时的输出不符合规范。

从该链接派生的最小示例:

(spec/fdef your-func
    :args even?
    :ret  string?)


(spec/instrument #'your-func)

这相当于设置一个函数有一个整数参数的前提条件和一个返回字符串的后置条件。除了你得到更多有用的错误,就像你正在寻找的那样。

官方指南中的更多详细信息:https : //clojure.org/guides/spec ---请参阅“规范功能”标题下。

于 2016-06-18T03:02:44.830 回答
2

在不考虑是否应该使用前置条件和后置条件来验证函数参数的情况下,有一种方法可以通过用 包装谓词来从前置条件和后置条件中打印出更清晰的消息clojure.test/is,如下面的答案所示:

如何让 Clojure :pre & :post 报告它们的失败值?

那么您的代码可能如下所示:

(ns pre-post-messages.core
  (:require [clojure.spec :as s]
            [clojure.test :as t]))

(defn person-name [person]
  {:pre [(t/is (s/valid? ::person person))]
   :post [(t/is (s/valid? string? %))]}
  (str (::first-name person) " " (::last-name person)))

(def try-1
  {:first-name "Anna Vissi"})

(def try-2
  {::first-name "Anna"
   ::last-name "Vissi"
   ::email "Anna@Vissi.com"})

(s/def ::person (s/keys :req [::first-name ::last-name ::email]))

评估

pre-post-messages.core> (person-name  try-2)

会产生

"Anna Vissi"

和评估

pre-post-messages.core> (person-name  try-1)

会产生

FAIL in () (core.clj:6)

expected: (s/valid? :pre-post-messages.core/person person)

  actual: (not (s/valid? :pre-post-messages.core/person {:first-name "Anna Vissi"}))

AssertionError Assert failed: (t/is (s/valid? :pre-post-messages.core/person person))  pre-post-messages.core/person-name (core.clj:5)
于 2016-06-18T18:41:59.120 回答
2

这在您不想使用s/assert或无法启用时很有用s/check-assserts。改进 MicSokoli 的回答:

:pre只关心返回的值都是真实的,因此我们可以将返回值转换"Success!\n"true(为了严格),throw并在输出不成功的情况下解释和输入数据出错。

(defn validate [spec input]
    (let [explanation (s/explain-str spec input)]
        (if (= explanation "Success!\n") 
            true
            (throw (ex-info explanation {:input input}))))

这个的一个变体可能是这个,但它会运行规范两次:

(defn validate [spec input]
    (if (s/valid? spec input) 
        true
        (throw (ex-info (s/explain spec input) {:input input}))))

用法:

(defn person-name [person]
  {:pre [(validate ::person person)]}
  (str (::first-name person) " " (::last-name person)))
于 2019-10-30T01:36:10.740 回答