test.check 中的循环和状态管理
Loops & state management in test.check
随着 Spec 的引入,我尝试为我的所有函数编写 test.check 生成器。这对于简单的数据结构来说很好,但对于具有相互依赖的部分的数据结构来说往往会变得困难。换句话说,生成器内部需要一些状态管理。
拥有 Clojure 的 loop/recur 或 reduce 的生成器等价物已经有很大的帮助,因此在一次迭代中产生的值可以存储在某个聚合值中,然后可以在后续迭代中访问。
One simple example where this would be required, is to write a generator for splitting up a collection into exactly X partitions, with each partition having between zero and Y elements, and where the elements are then randomly assigned to any of the partitions. (Note that test.chuck
's partition
function does not allow to specify X or Y).
If you write this generator by looping through the collection, then this would require access to the partitions filled up during previous iterations, to avoid exceeding Y.
有人有什么想法吗?我找到的部分解决方案:
test.check 的 let
和 bind
允许您生成一个值,然后在以后重用该值,但它们不允许迭代。
您可以使用 tuple
和 bind
函数的组合来遍历以前生成的值的集合,但是这些迭代无法访问期间生成的值之前的迭代。
(defn bind-each [k coll] (apply tcg/tuple (map (fn [x] (tcg/bind (tcg/return x) k)) coll))
您可以使用 atoms(或 volatiles)来存储和访问先前迭代中生成的值。这有效,但非常不符合 Clojure,特别是因为您需要在生成器返回之前 reset!
atom/volatile,以避免它们的内容在生成器的下一次调用中被重用。
生成器由于其 bind
和 return
函数而类似于 monad,这暗示了将诸如 Cats 之类的 monad 库与 State monad 结合使用。但是,State monad 在 Cats 2.0 中被删除了(因为据称它不适合 Clojure),而我知道的其他支持库没有正式的 Clojurescript 支持。此外,当在他自己的库中实现 State monad 时,Jim Duey——Clojure 的 monad 专家之一——似乎警告说 State monad 的使用与 test.check 的收缩不兼容(见 http://www.clojure.net/2015/09/11/Extending-Generative-Testing/), 这显着降低了使用 test.check.
的优点
您可以通过将 gen/let
(或等效的 gen/bind
)与显式递归相结合来完成您所描述的迭代:
(defn make-foo-generator
[state]
(if (good-enough? state)
(gen/return state)
(gen/let [state' (gen-next-step state)]
(make-foo-generator state'))))
但是,如果可能的话,值得尝试避免这种模式,因为每次使用 let
/bind
都会破坏收缩过程。有时可以使用 gen/fmap
重新组织生成器。例如,要将一个集合划分为一系列 X 个子集(我意识到这与您的示例不完全相同,但我认为可以对其进行调整以适应),您可以这样做:
(defn partition
[coll subset-count]
(gen/let [idxs (gen/vector (gen/choose 0 (dec subset-count))
(count coll))]
(->> (map vector coll idxs)
(group-by second)
(sort-by key)
(map (fn [[_ pairs]] (map first pairs))))))
随着 Spec 的引入,我尝试为我的所有函数编写 test.check 生成器。这对于简单的数据结构来说很好,但对于具有相互依赖的部分的数据结构来说往往会变得困难。换句话说,生成器内部需要一些状态管理。
拥有 Clojure 的 loop/recur 或 reduce 的生成器等价物已经有很大的帮助,因此在一次迭代中产生的值可以存储在某个聚合值中,然后可以在后续迭代中访问。
One simple example where this would be required, is to write a generator for splitting up a collection into exactly X partitions, with each partition having between zero and Y elements, and where the elements are then randomly assigned to any of the partitions. (Note that
test.chuck
'spartition
function does not allow to specify X or Y).If you write this generator by looping through the collection, then this would require access to the partitions filled up during previous iterations, to avoid exceeding Y.
有人有什么想法吗?我找到的部分解决方案:
test.check 的
let
和bind
允许您生成一个值,然后在以后重用该值,但它们不允许迭代。您可以使用
tuple
和bind
函数的组合来遍历以前生成的值的集合,但是这些迭代无法访问期间生成的值之前的迭代。(defn bind-each [k coll] (apply tcg/tuple (map (fn [x] (tcg/bind (tcg/return x) k)) coll))
您可以使用 atoms(或 volatiles)来存储和访问先前迭代中生成的值。这有效,但非常不符合 Clojure,特别是因为您需要在生成器返回之前
reset!
atom/volatile,以避免它们的内容在生成器的下一次调用中被重用。生成器由于其
的优点bind
和return
函数而类似于 monad,这暗示了将诸如 Cats 之类的 monad 库与 State monad 结合使用。但是,State monad 在 Cats 2.0 中被删除了(因为据称它不适合 Clojure),而我知道的其他支持库没有正式的 Clojurescript 支持。此外,当在他自己的库中实现 State monad 时,Jim Duey——Clojure 的 monad 专家之一——似乎警告说 State monad 的使用与 test.check 的收缩不兼容(见 http://www.clojure.net/2015/09/11/Extending-Generative-Testing/), 这显着降低了使用 test.check.
您可以通过将 gen/let
(或等效的 gen/bind
)与显式递归相结合来完成您所描述的迭代:
(defn make-foo-generator
[state]
(if (good-enough? state)
(gen/return state)
(gen/let [state' (gen-next-step state)]
(make-foo-generator state'))))
但是,如果可能的话,值得尝试避免这种模式,因为每次使用 let
/bind
都会破坏收缩过程。有时可以使用 gen/fmap
重新组织生成器。例如,要将一个集合划分为一系列 X 个子集(我意识到这与您的示例不完全相同,但我认为可以对其进行调整以适应),您可以这样做:
(defn partition
[coll subset-count]
(gen/let [idxs (gen/vector (gen/choose 0 (dec subset-count))
(count coll))]
(->> (map vector coll idxs)
(group-by second)
(sort-by key)
(map (fn [[_ pairs]] (map first pairs))))))