使用 clojure.spec 进行强制转换是惯用的吗?
Is use of clojure.spec for coercion idiomatic?
我已经看到使用 clojure conformers 来强制转换各种要点中的数据,但也有一个印象(我不记得在哪里)强制转换(例如如下)不是 conformers 的惯用用法。
(s/def :conformers/int
(s/conformer (fn [x]
(cond
(integer? x) x
(re-matches #"\d+" x) (edn/read-string x)
:else :cljs.spec.alpha/invalid))))
(s/def :data.subscription/quantity :conformers/int)
(s/def :data/subscription (s/keys :req-un [:data.subscription/quantity]))
以上是真的unidiomatic/unintended吗?如果是这样,appropriate/idiomatic 有什么用。预期用途的界限在哪里?
更新:
现在我已经发布了一个库来处理这个问题,请查看:https://github.com/wilkerlucio/spec-coerce
您可以使用 spec 进行强制转换,但重要的是您还拥有它的非强制转换版本。如果你对你的规范强制强制执行,那么你将同时做两件事,这违反了 SRP。所以建议是有一个简单的验证版本,然后你可以在它上面再做一个,所以以后你可以选择是使用强制版本还是简单验证版本。
另一种选择(我更喜欢)是基于规范 运行 并行使用强制转换引擎。如果您查看 specs 如何从 specs (check here) 推断生成器,您会发现您可以使用 spec 形式导出其他东西,所以这个其他东西可以作为您的强制引擎。
我写了一篇文章解释了如何做到这一点,你可以在这里找到它(只需跳转到规范强制转换部分):https://medium.com/@wilkerlucio/implementing-custom-om-next-parsers-f20ca6db1664
文章中摘录的代码供参考:
(def built-in-coercions
{`int? #(Long/parseLong %)
`nat-int? #(Long/parseLong %)
`pos-int? #(Long/parseLong %)
`inst? clojure.instant/read-instant-timestamp})
(defn spec->coerce-sym [spec]
(try (s/form spec) (catch Exception _ nil)))
(defn coerce [key value]
(let [form (spec->coerce-sym key)
coerce-fn (get built-in-coercions form identity)]
(if (string? value)
(coerce-fn value)
value)))
这里还有一个更详细的版本(只是代码),其中包括一个辅助注册表,因此您可以设置特定的强制器以匹配相同的规范关键字:https://gist.github.com/wilkerlucio/08fb5f858a12b86f63c76cf453cd90c0
这样您就不会强制执行强制转换,从而加快验证速度并让您更好地控制何时强制转换(通常只应在系统边界发生)。
虽然规格设计仍在完善中,但要给出明确的答案还为时过早。所以我使用这个定义,源自标准库如何使用 conforming:
强制转换是隐式自动转换为下游预期的形状。
Conforming 采用一个已经具有预期形状的值,并生成从该值和规范一起派生的编程信息(因此 con 形式)。根据规范,不保证符合结果是有效的。例如基于 s/or
的规范或基于 Regex 的规范。
简而言之:符合不是惯用的强制,而是看起来相似的其他东西。
我预计规范中的强制转换有时会成为一个单独的功能。
Clojure.spec 不用于强制转换
根据编写 clojure.spec 的团队,将其用于强制不是惯用的。自行承担设计和工程风险。
Cognitect clojure.core 团队的 Alex Miller 重申了 Clojure 邮件列表中的官方立场 20 February 2018:
We recommend that you not use conformers for coercion.
他暗示了他们的原因:稍后在同一个线程中,他谈到了一个在规范之上构建强制的库,它 "combines specs for your desired output with a coercion function, making the spec of the actual data implicit." 这种混淆不是 [=36 的预期用途的一部分=].
但是...如果不符合规范,如何强制执行?答案是,普通的旧 Clojure 函数,就像我们一直在做的那样。再次来自 Alex Miller (16 December 2016):
If you really need to do a wholesale coercion of all your attributes, it seems like you can do this explicitly as a pre-step before validating the map with spec and that may be the better way to go. I'm not sure what spec is buying you over just explicitly transforming the map using normal Clojure functions?
...为什么不呢?
诸如 spec 之类的编程合同表示在系统边界使用的各方之间的协议。这些 specs/contracts/agreements 是 intended 用于验证和错误检查、测试(尤其是生成)和文档(尤其是在错误时)。关于什么数据 应该 的协议绝对不同于将不符合的数据转换为符合的数据的行为。它们是两个不同的问题,尽管它们可能经常发生在彼此附近。这两个概念的这种相邻性使得不要混淆它们尤为重要。
我已经看到使用 clojure conformers 来强制转换各种要点中的数据,但也有一个印象(我不记得在哪里)强制转换(例如如下)不是 conformers 的惯用用法。
(s/def :conformers/int
(s/conformer (fn [x]
(cond
(integer? x) x
(re-matches #"\d+" x) (edn/read-string x)
:else :cljs.spec.alpha/invalid))))
(s/def :data.subscription/quantity :conformers/int)
(s/def :data/subscription (s/keys :req-un [:data.subscription/quantity]))
以上是真的unidiomatic/unintended吗?如果是这样,appropriate/idiomatic 有什么用。预期用途的界限在哪里?
更新:
现在我已经发布了一个库来处理这个问题,请查看:https://github.com/wilkerlucio/spec-coerce
您可以使用 spec 进行强制转换,但重要的是您还拥有它的非强制转换版本。如果你对你的规范强制强制执行,那么你将同时做两件事,这违反了 SRP。所以建议是有一个简单的验证版本,然后你可以在它上面再做一个,所以以后你可以选择是使用强制版本还是简单验证版本。
另一种选择(我更喜欢)是基于规范 运行 并行使用强制转换引擎。如果您查看 specs 如何从 specs (check here) 推断生成器,您会发现您可以使用 spec 形式导出其他东西,所以这个其他东西可以作为您的强制引擎。
我写了一篇文章解释了如何做到这一点,你可以在这里找到它(只需跳转到规范强制转换部分):https://medium.com/@wilkerlucio/implementing-custom-om-next-parsers-f20ca6db1664
文章中摘录的代码供参考:
(def built-in-coercions
{`int? #(Long/parseLong %)
`nat-int? #(Long/parseLong %)
`pos-int? #(Long/parseLong %)
`inst? clojure.instant/read-instant-timestamp})
(defn spec->coerce-sym [spec]
(try (s/form spec) (catch Exception _ nil)))
(defn coerce [key value]
(let [form (spec->coerce-sym key)
coerce-fn (get built-in-coercions form identity)]
(if (string? value)
(coerce-fn value)
value)))
这里还有一个更详细的版本(只是代码),其中包括一个辅助注册表,因此您可以设置特定的强制器以匹配相同的规范关键字:https://gist.github.com/wilkerlucio/08fb5f858a12b86f63c76cf453cd90c0
这样您就不会强制执行强制转换,从而加快验证速度并让您更好地控制何时强制转换(通常只应在系统边界发生)。
虽然规格设计仍在完善中,但要给出明确的答案还为时过早。所以我使用这个定义,源自标准库如何使用 conforming:
强制转换是隐式自动转换为下游预期的形状。
Conforming 采用一个已经具有预期形状的值,并生成从该值和规范一起派生的编程信息(因此 con 形式)。根据规范,不保证符合结果是有效的。例如基于 s/or
的规范或基于 Regex 的规范。
简而言之:符合不是惯用的强制,而是看起来相似的其他东西。
我预计规范中的强制转换有时会成为一个单独的功能。
Clojure.spec 不用于强制转换
根据编写 clojure.spec 的团队,将其用于强制不是惯用的。自行承担设计和工程风险。
Cognitect clojure.core 团队的 Alex Miller 重申了 Clojure 邮件列表中的官方立场 20 February 2018:
We recommend that you not use conformers for coercion.
他暗示了他们的原因:稍后在同一个线程中,他谈到了一个在规范之上构建强制的库,它 "combines specs for your desired output with a coercion function, making the spec of the actual data implicit." 这种混淆不是 [=36 的预期用途的一部分=].
但是...如果不符合规范,如何强制执行?答案是,普通的旧 Clojure 函数,就像我们一直在做的那样。再次来自 Alex Miller (16 December 2016):
If you really need to do a wholesale coercion of all your attributes, it seems like you can do this explicitly as a pre-step before validating the map with spec and that may be the better way to go. I'm not sure what spec is buying you over just explicitly transforming the map using normal Clojure functions?
...为什么不呢?
诸如 spec 之类的编程合同表示在系统边界使用的各方之间的协议。这些 specs/contracts/agreements 是 intended 用于验证和错误检查、测试(尤其是生成)和文档(尤其是在错误时)。关于什么数据 应该 的协议绝对不同于将不符合的数据转换为符合的数据的行为。它们是两个不同的问题,尽管它们可能经常发生在彼此附近。这两个概念的这种相邻性使得不要混淆它们尤为重要。