这个问题是在这次讨论的上下文中发现的
https://groups.google.com/d/msg/clojure/mIlKaOiujlo/tF71zZ2BCwAJ
规格错误报告失败并对用户直觉产生误导的最小版本如下:
他使用了一个无效的ns形式
(ns foo (require [clojure.spec :as s])) ; 应为 :require
规格报告的错误
在:[1] val:((require [clojure.spec :as s])) 在:[:args] 中失败,断言:(:docstring (? string?) :attr-map (? map?) :clauses :clojure.core.specs/ns-clauses),额外输入
:clojure.spec/args (foo (require [clojure.spec :as s]))
clojure.core/ex-info (core.clj:4725)
虽然这个错误在技术上是对的,但它没有显示用户所报告的每个s/cat替代选项失败的原因。
为了更好地理解为什么用户的数据不正确,他应该确切知道规格尝试了什么以及是如何失败的。
一个理解此过程的好例子是s/alt,其中所有失败的替代选项总是报告给用户。
这个问题已经得到了调查,首先是实验性的,然后在规格代码中,最后附带了类似于s/alts的错误报告补丁。
观察到 especs 对 cat 带有可选分支的错误报告行为如下:
1. 如果 cat 在一个或多个可选分支之后失败,则整个 cat 被报告为失败。
2. 如果 cat 在一个或多个可选分支之后/并且/随后的必需分支失败,则只报告随后的必需分支,而对替代可选分支不进行任何说明。
规则1解释了ns示例。
规则2可能严重误导用户的直觉
(s/explain (s/cat :maybe-num (s/? number?)
["3"])
返回
在:[0] val:`"3"` 在:[:keyword] 中失败,断言:keyword?
报告显然没有针对用户希望输入数字的意图。相反,他可能会认为他应该输入一个关键字。
解决方案
编写了一个简单的补丁,将op-explain 更改为以下行为
- 在 s/cat 中尝试的所有替代选项都分别报告。
这显著改进了报告的错误,因为它清楚地展示了用户数据是如何未通过验证的。
(ns foo (require [clojure.spec :as s])) ; 应为 :require
现在给出
ExceptionInfo 对 clojure.core/ns 的调用未符合规范
在:[1] val:require([clojure.spec :as s]) 在:[:args :docstring] 中失败,断言:string?
输入:[1] val:(require '[clojure.spec :as s]) 失败在:[:args :attr-map],断言:map?
输入:[1 0] val:require 失败规范::clojure.core.specs/ns-refer-clojure 在:[:args :clauses :refer-clojure :clause],断言:#{:refer-clojure}
输入:[1 0] val:require 失败规范::clojure.core.specs/ns-require 在:[:args :clauses :require :clause],断言:#{:require}
输入:[1 0] val:require 失败规范::clojure.core.specs/ns-import 在:[:args :clauses :import :clause],断言:#{:import}
输入:[1 0] val:require 失败规范::clojure.core.specs/ns-use 在:[:args :clauses :use :clause],断言:#{:use}
输入:[1 0] val:require 失败规范::clojure.core.specs/ns-refer 在:[:args :clauses :refer :clause],断言:#{:refer}
输入:[1 0] val:require 失败规范::clojure.core.specs/ns-load 在:[:args :clauses :load :clause],断言:#{:load}
输入:[1 0] val:require 失败规范::clojure.core.specs/ns-gen-class 在:[:args :clauses :gen-class :clause],断言:#{:gen-class}
:clojure.spec/args (foo (require [clojure.spec :as s]))
clojure.core/ex-info (core.clj:4725)
如果 explain-data 能按 :path 的长度对 ::s/problems 排序会更理想,这将会把前两个非预期选项推到最后。
(s/explain (s/cat :maybe-num (s/? number?)
["3"])
现在给出
输入:[0] val:"3" 失败在:[:maybe-num],断言:number?
在:[0] val:`"3"` 在:[:keyword] 中失败,断言:keyword?
虽然可以编造例子,让这种报告产生更多噪音(如 defn),但我相信这在上文所述原因和
- 当出错时,我们程序员总是要求用户提供最具体的信息 - 将这种做法应用于规范错误报告也是正确的
- 自定义错误报告器 (s/*explain-out*) 获取更多数据来生成更符合用户意图的狭窄报告