关键字参数的 Plumatic Schema
Plumatic Schema for keyword arguments
假设我们有一个函数 get-ints
,带有一个位置参数、调用者想要的整数个数,以及两个命名参数 :max
和 :min
,例如:
; Ignore that the implementation of the function is incorrect.
(defn get-ints [nr & {:keys [max min] :or {max 10 min 0}}]
(take nr (repeatedly #(int (+ (* (rand) (- max min -1)) min)))))
(get-ints 5) ; => (8 4 10 5 5)
(get-ints 5 :max 100) ; => (78 43 32 66 6)
(get-ints 5 :min 5) ; => (10 5 9 9 9)
(get-ints 5 :min 5 :max 6) ; => (5 5 6 6 5)
如何为 get-ints
的参数列表编写一个 Plumatic Schema,一个包含一个、三个或五个项目的列表,其中第一个总是一个数字,而后面的项目总是成对的关键字和关联值。
对于 Clojure Spec,我将其表示为:
(require '[clojure.spec :as spec])
(spec/cat :nr pos-int? :args (spec/keys* :opt-un [::min ::max]))
以及 ::min
和 ::max
持有的有效值的单独定义。
我认为在这种情况下,编写您需要的特定代码比尝试使用 Plumatic Schema 或并非专为该用例设计的其他工具强制适合解决方案更容易。请记住,Plumatic Schema 和其他工具(如内置的 Clojure 前置条件和 post 条件)只是在违反某些条件时抛出 Exception
的 shorthand 方式。如果这些 DSL 中的 none 是合适的,那么您总有可以依靠的通用语言。
对于 rel=
函数,in the Tupelo library 可以找到与您类似的情况。它旨在对两个数字之间的 "relative equality" 进行测试。它是这样工作的:
(is (rel= 123450000 123456789 :digits 4 )) ; .12345 * 10^9
(is (not (rel= 123450000 123456789 :digits 6 )))
(is (rel= 0.123450000 0.123456789 :digits 4 )) ; .12345 * 1
(is (not (rel= 0.123450000 0.123456789 :digits 6 )))
(is (rel= 1 1.001 :tol 0.01 )) ; :tol value is absolute error
(is (not (rel= 1 1.001 :tol 0.0001 )))
虽然 Tupelo 库中的几乎所有其他函数都大量使用 Plumatic Schema,但这个函数做到了 "manually":
(defn rel=
"Returns true if 2 double-precision numbers are relatively equal, else false. Relative equality
is specified as either (1) the N most significant digits are equal, or (2) the absolute
difference is less than a tolerance value. Input values are coerced to double before comparison.
Example:
(rel= 123450000 123456789 :digits 4 ) ; true
(rel= 1 1.001 :tol 0.01) ; true
"
[val1 val2 & {:as opts}]
{:pre [(number? val1) (number? val2)]
:post [(contains? #{true false} %)]}
(let [{:keys [digits tol]} opts]
(when-not (or digits tol)
(throw (IllegalArgumentException.
(str "Must specify either :digits or :tol" \newline
"opts: " opts))))
(when tol
(when-not (number? tol)
(throw (IllegalArgumentException.
(str ":tol must be a number" \newline
"opts: " opts))))
(when-not (pos? tol)
(throw (IllegalArgumentException.
(str ":tol must be positive" \newline
"opts: " opts)))))
(when digits
(when-not (integer? digits)
(throw (IllegalArgumentException.
(str ":digits must be an integer" \newline
"opts: " opts))))
(when-not (pos? digits)
(throw (IllegalArgumentException.
(str ":digits must positive" \newline
"opts: " opts)))))
; At this point, there were no invalid args and at least one of
; either :tol and/or :digits was specified. So, return the answer.
(let [val1 (double val1)
val2 (double val2)
delta-abs (Math/abs (- val1 val2))
or-result (truthy?
(or (zero? delta-abs)
(and tol
(let [tol-result (< delta-abs tol)]
tol-result))
(and digits
(let [abs1 (Math/abs val1)
abs2 (Math/abs val2)
max-abs (Math/max abs1 abs2)
delta-rel-abs (/ delta-abs max-abs)
rel-tol (Math/pow 10 (- digits))
dig-result (< delta-rel-abs rel-tol)]
dig-result))))
]
or-result)))
根据我从 Plumatic 邮件列表 [0] [1] 得到的答案,我坐下来在模式语言本身之外编写了我自己的 conformer:
(defn key-val-seq?
([kv-seq]
(and (even? (count kv-seq))
(every? keyword? (take-nth 2 kv-seq))))
([kv-seq validation-map]
(and (key-val-seq? kv-seq)
(every? nil? (for [[k v] (partition 2 kv-seq)]
(if-let [schema (get validation-map k)]
(schema/check schema v)
:schema/invalid))))))
(def get-int-args
(schema/constrained
[schema/Any]
#(and (integer? (first %))
(key-val-seq? (rest %) {:max schema/Int :min schema/Int}))))
(schema/validate get-int-args '()) ; Exception: Value does not match schema...
(schema/validate get-int-args '(5)) ; => (5)
(schema/validate get-int-args [5 :max 10]) ; => [5 :max 10]
(schema/validate get-int-args [5 :max 10 :min 1]); => [5 :max 10 :min 1]
(schema/validate get-int-args [5 :max 10 :b 1]) ; Exception: Value does not match schema...
假设我们有一个函数 get-ints
,带有一个位置参数、调用者想要的整数个数,以及两个命名参数 :max
和 :min
,例如:
; Ignore that the implementation of the function is incorrect.
(defn get-ints [nr & {:keys [max min] :or {max 10 min 0}}]
(take nr (repeatedly #(int (+ (* (rand) (- max min -1)) min)))))
(get-ints 5) ; => (8 4 10 5 5)
(get-ints 5 :max 100) ; => (78 43 32 66 6)
(get-ints 5 :min 5) ; => (10 5 9 9 9)
(get-ints 5 :min 5 :max 6) ; => (5 5 6 6 5)
如何为 get-ints
的参数列表编写一个 Plumatic Schema,一个包含一个、三个或五个项目的列表,其中第一个总是一个数字,而后面的项目总是成对的关键字和关联值。
对于 Clojure Spec,我将其表示为:
(require '[clojure.spec :as spec])
(spec/cat :nr pos-int? :args (spec/keys* :opt-un [::min ::max]))
以及 ::min
和 ::max
持有的有效值的单独定义。
我认为在这种情况下,编写您需要的特定代码比尝试使用 Plumatic Schema 或并非专为该用例设计的其他工具强制适合解决方案更容易。请记住,Plumatic Schema 和其他工具(如内置的 Clojure 前置条件和 post 条件)只是在违反某些条件时抛出 Exception
的 shorthand 方式。如果这些 DSL 中的 none 是合适的,那么您总有可以依靠的通用语言。
对于 rel=
函数,in the Tupelo library 可以找到与您类似的情况。它旨在对两个数字之间的 "relative equality" 进行测试。它是这样工作的:
(is (rel= 123450000 123456789 :digits 4 )) ; .12345 * 10^9
(is (not (rel= 123450000 123456789 :digits 6 )))
(is (rel= 0.123450000 0.123456789 :digits 4 )) ; .12345 * 1
(is (not (rel= 0.123450000 0.123456789 :digits 6 )))
(is (rel= 1 1.001 :tol 0.01 )) ; :tol value is absolute error
(is (not (rel= 1 1.001 :tol 0.0001 )))
虽然 Tupelo 库中的几乎所有其他函数都大量使用 Plumatic Schema,但这个函数做到了 "manually":
(defn rel=
"Returns true if 2 double-precision numbers are relatively equal, else false. Relative equality
is specified as either (1) the N most significant digits are equal, or (2) the absolute
difference is less than a tolerance value. Input values are coerced to double before comparison.
Example:
(rel= 123450000 123456789 :digits 4 ) ; true
(rel= 1 1.001 :tol 0.01) ; true
"
[val1 val2 & {:as opts}]
{:pre [(number? val1) (number? val2)]
:post [(contains? #{true false} %)]}
(let [{:keys [digits tol]} opts]
(when-not (or digits tol)
(throw (IllegalArgumentException.
(str "Must specify either :digits or :tol" \newline
"opts: " opts))))
(when tol
(when-not (number? tol)
(throw (IllegalArgumentException.
(str ":tol must be a number" \newline
"opts: " opts))))
(when-not (pos? tol)
(throw (IllegalArgumentException.
(str ":tol must be positive" \newline
"opts: " opts)))))
(when digits
(when-not (integer? digits)
(throw (IllegalArgumentException.
(str ":digits must be an integer" \newline
"opts: " opts))))
(when-not (pos? digits)
(throw (IllegalArgumentException.
(str ":digits must positive" \newline
"opts: " opts)))))
; At this point, there were no invalid args and at least one of
; either :tol and/or :digits was specified. So, return the answer.
(let [val1 (double val1)
val2 (double val2)
delta-abs (Math/abs (- val1 val2))
or-result (truthy?
(or (zero? delta-abs)
(and tol
(let [tol-result (< delta-abs tol)]
tol-result))
(and digits
(let [abs1 (Math/abs val1)
abs2 (Math/abs val2)
max-abs (Math/max abs1 abs2)
delta-rel-abs (/ delta-abs max-abs)
rel-tol (Math/pow 10 (- digits))
dig-result (< delta-rel-abs rel-tol)]
dig-result))))
]
or-result)))
根据我从 Plumatic 邮件列表 [0] [1] 得到的答案,我坐下来在模式语言本身之外编写了我自己的 conformer:
(defn key-val-seq?
([kv-seq]
(and (even? (count kv-seq))
(every? keyword? (take-nth 2 kv-seq))))
([kv-seq validation-map]
(and (key-val-seq? kv-seq)
(every? nil? (for [[k v] (partition 2 kv-seq)]
(if-let [schema (get validation-map k)]
(schema/check schema v)
:schema/invalid))))))
(def get-int-args
(schema/constrained
[schema/Any]
#(and (integer? (first %))
(key-val-seq? (rest %) {:max schema/Int :min schema/Int}))))
(schema/validate get-int-args '()) ; Exception: Value does not match schema...
(schema/validate get-int-args '(5)) ; => (5)
(schema/validate get-int-args [5 :max 10]) ; => [5 :max 10]
(schema/validate get-int-args [5 :max 10 :min 1]); => [5 :max 10 :min 1]
(schema/validate get-int-args [5 :max 10 :b 1]) ; Exception: Value does not match schema...