具有命名参数的函数的现实 Clojure 规范
Realistic Clojure Spec for function with named arguments
假设我们有一个函数 clothe
除了一些可选的命名参数 :hat
、:shirt
和 [=18] 之外还需要一个位置参数 person
=].
(defn clothe [person & {:keys [hat shirt pants]}]
(str "Clothing " person " with " hat shirt pants "."))
(clothe 'me :hat "top hat")
=> "Clothing me with top hat."
我目前为此功能编写规范的方式是:
(require '[clojure.spec :as spec]
'[clojure.spec.gen :as gen])
(spec/def ::person symbol?)
(spec/def ::clothing
(spec/alt :hat (spec/cat :key #{:hat} :value string?)
:shirt (spec/cat :key #{:shirt} :value string?)
:pants (spec/cat :key #{:pants} :value string?)))
(spec/fdef clothe
:args (spec/cat :person ::person
:clothes (spec/* ::clothing))
:ret string?)
问题是它允许像
这样的参数列表
(clothe 'me :hat "top hat" :hat "nice hat")
=> "Clothing me with nice hat."
尽管语言本身允许,但无论何时犯错都可能是错误的。但也许比这更糟糕的是,它使生成的数据与通常调用函数的方式不切实际:
(gen/generate (spec/gen (spec/cat :person ::person
:clothes (spec/* ::clothing))))
=> (_+_6+h/!-6Gg9!43*e :hat "m6vQmoR72CXc6R3GP2hcdB5a0"
:hat "05G5884aBLc80s4AF5X9V84u4RW" :pants "3Q" :pants "a0v329r25f3k5oJ4UZJJQa5"
:hat "C5h2HW34LG732ifPQDieH" :pants "4aeBas8uWx1eQWYpLRezBIR" :hat "C229mzw"
:shirt "Hgw3EgUZKF7c7ya6q2fqW249GsB" :pants "byG23H2XyMTx0P7v5Ve9qBs"
:shirt "5wPMjn1F2X84lU7X3CtfalPknQ5" :pants "0M5TBgHQ4lR489J55atm11F3"
:shirt "FKn5vMjoIayO" :shirt "2N9xKcIbh66" :hat "K8xSFeydF" :hat "sQY4iUPF0Ef58198270DOf"
:hat "gHGEqi58A4pH2s74t0" :pants "" :hat "D6RKWJJoFLCAaHId8AF4" :pants "exab2w5o88b"
:hat "S7Ti2Cb1f7se7o86I1uE" :shirt "9g3K6q1" :hat "slKjK67608Y9w1sqV1Kxm"
:hat "cFbVMaq8bfP22P8cD678s" :hat "f57" :hat "2W83oa0WVWM10y1U49265k2bJx"
:hat "O6" :shirt "7BUJ824efBb81RL99zBrvH2HjziIT")
更糟糕的是,如果您碰巧使用 spec/*
进行递归定义,则无法限制在对代码进行 运行 测试时生成的潜在递归次数。
那么我的问题就变成了:有没有办法为函数指定命名参数,将每个键的出现次数限制为一个?
如果我们查看 require
宏在 clojure.core.specs
中的指定方式,我们可以看到它使用 (spec/keys* :opt-un [])
来指定依赖列表中的命名参数,例如 :refer
和 :as
在 (ns (:require [a.b :as b :refer :all]))
.
(s/def ::or (s/map-of simple-symbol? any?))
(s/def ::as ::local-name)
(s/def ::prefix-list
(s/spec
(s/cat :prefix simple-symbol?
:suffix (s/* (s/alt :lib simple-symbol? :prefix-list ::prefix-list))
:refer (s/keys* :opt-un [::as ::refer]))))
(s/def ::ns-require
(s/spec (s/cat :clause #{:require}
:libs (s/* (s/alt :lib simple-symbol?
:prefix-list ::prefix-list
:flag #{:reload :reload-all :verbose})))))
文档没有提到 :req-un
和 :opt-un
的用途,但规范指南另一方面提到它们用于指定不合格的键。回到我们的函数定义,我们可以把它写成:
(spec/def ::clothing (spec/keys* :opt-un [::hat ::shirt ::pants]))
(spec/def ::hat string?)
(spec/def ::shirt string?)
(spec/def ::pants string?)
(spec/fdef clothe
:args (spec/cat :person ::person
:clothes ::clothing)
:ret string?)
遗憾的是,这对接受同一命名参数的多个实例的函数没有帮助
(stest/instrument `clothe)
(clothe 'me :hat "top hat" :hat "nice hat")
=> "Clothing me with nice hat."
尽管这确实意味着生成器最多生成一个相同密钥的实例,这确实有助于递归规范。
(gen/generate (spec/gen (spec/cat :person ::person
:clothes ::clothing)))
=> (u_K_P6!!?4Ok!_I.-.d!2_.T-0.!+H+/At.7R8z*6?QB+921A
:shirt "B4W86P637c6KAK1rv04O4FRn6S" :pants "3gdkiY" :hat "20o77")
假设我们有一个函数 clothe
除了一些可选的命名参数 :hat
、:shirt
和 [=18] 之外还需要一个位置参数 person
=].
(defn clothe [person & {:keys [hat shirt pants]}]
(str "Clothing " person " with " hat shirt pants "."))
(clothe 'me :hat "top hat")
=> "Clothing me with top hat."
我目前为此功能编写规范的方式是:
(require '[clojure.spec :as spec]
'[clojure.spec.gen :as gen])
(spec/def ::person symbol?)
(spec/def ::clothing
(spec/alt :hat (spec/cat :key #{:hat} :value string?)
:shirt (spec/cat :key #{:shirt} :value string?)
:pants (spec/cat :key #{:pants} :value string?)))
(spec/fdef clothe
:args (spec/cat :person ::person
:clothes (spec/* ::clothing))
:ret string?)
问题是它允许像
这样的参数列表(clothe 'me :hat "top hat" :hat "nice hat")
=> "Clothing me with nice hat."
尽管语言本身允许,但无论何时犯错都可能是错误的。但也许比这更糟糕的是,它使生成的数据与通常调用函数的方式不切实际:
(gen/generate (spec/gen (spec/cat :person ::person
:clothes (spec/* ::clothing))))
=> (_+_6+h/!-6Gg9!43*e :hat "m6vQmoR72CXc6R3GP2hcdB5a0"
:hat "05G5884aBLc80s4AF5X9V84u4RW" :pants "3Q" :pants "a0v329r25f3k5oJ4UZJJQa5"
:hat "C5h2HW34LG732ifPQDieH" :pants "4aeBas8uWx1eQWYpLRezBIR" :hat "C229mzw"
:shirt "Hgw3EgUZKF7c7ya6q2fqW249GsB" :pants "byG23H2XyMTx0P7v5Ve9qBs"
:shirt "5wPMjn1F2X84lU7X3CtfalPknQ5" :pants "0M5TBgHQ4lR489J55atm11F3"
:shirt "FKn5vMjoIayO" :shirt "2N9xKcIbh66" :hat "K8xSFeydF" :hat "sQY4iUPF0Ef58198270DOf"
:hat "gHGEqi58A4pH2s74t0" :pants "" :hat "D6RKWJJoFLCAaHId8AF4" :pants "exab2w5o88b"
:hat "S7Ti2Cb1f7se7o86I1uE" :shirt "9g3K6q1" :hat "slKjK67608Y9w1sqV1Kxm"
:hat "cFbVMaq8bfP22P8cD678s" :hat "f57" :hat "2W83oa0WVWM10y1U49265k2bJx"
:hat "O6" :shirt "7BUJ824efBb81RL99zBrvH2HjziIT")
更糟糕的是,如果您碰巧使用 spec/*
进行递归定义,则无法限制在对代码进行 运行 测试时生成的潜在递归次数。
那么我的问题就变成了:有没有办法为函数指定命名参数,将每个键的出现次数限制为一个?
如果我们查看 require
宏在 clojure.core.specs
中的指定方式,我们可以看到它使用 (spec/keys* :opt-un [])
来指定依赖列表中的命名参数,例如 :refer
和 :as
在 (ns (:require [a.b :as b :refer :all]))
.
(s/def ::or (s/map-of simple-symbol? any?))
(s/def ::as ::local-name)
(s/def ::prefix-list
(s/spec
(s/cat :prefix simple-symbol?
:suffix (s/* (s/alt :lib simple-symbol? :prefix-list ::prefix-list))
:refer (s/keys* :opt-un [::as ::refer]))))
(s/def ::ns-require
(s/spec (s/cat :clause #{:require}
:libs (s/* (s/alt :lib simple-symbol?
:prefix-list ::prefix-list
:flag #{:reload :reload-all :verbose})))))
文档没有提到 :req-un
和 :opt-un
的用途,但规范指南另一方面提到它们用于指定不合格的键。回到我们的函数定义,我们可以把它写成:
(spec/def ::clothing (spec/keys* :opt-un [::hat ::shirt ::pants]))
(spec/def ::hat string?)
(spec/def ::shirt string?)
(spec/def ::pants string?)
(spec/fdef clothe
:args (spec/cat :person ::person
:clothes ::clothing)
:ret string?)
遗憾的是,这对接受同一命名参数的多个实例的函数没有帮助
(stest/instrument `clothe)
(clothe 'me :hat "top hat" :hat "nice hat")
=> "Clothing me with nice hat."
尽管这确实意味着生成器最多生成一个相同密钥的实例,这确实有助于递归规范。
(gen/generate (spec/gen (spec/cat :person ::person
:clothes ::clothing)))
=> (u_K_P6!!?4Ok!_I.-.d!2_.T-0.!+H+/At.7R8z*6?QB+921A
:shirt "B4W86P637c6KAK1rv04O4FRn6S" :pants "3gdkiY" :hat "20o77")