3

随着 Spec 的引入,我尝试为我的所有函数编写 test.check 生成器。这对于简单的数据结构来说很好,但对于具有相互依赖的部分的数据结构来说往往会变得困难。换句话说,然后需要在生成器中进行一些状态管理。

拥有 Clojure 的循环/递归或归约的生成器等效项已经非常有帮助,因此在一次迭代中产生的值可以存储在某个聚合值中,然后在后续迭代中可以访问。

需要这样做的一个简单示例是编写一个生成器,用于将集合拆分为恰好 X 个分区,每个分区具有零到 Y 之间的元素,然后将元素随机分配给任何分区。(注意test.chuck'partition函数不允许指定 X 或 Y)。

如果您通过循环遍历集合来编写此生成器,那么这将需要访问在先前迭代期间填充的分区,以避免超过 Y。

有人有什么想法吗?我发现的部分解决方案:

  • test.checkletbind允许您生成一个值,然后稍后重用该值,但它们不允许迭代。

  • tuple您可以使用和函数的组合遍历先前生成的值的集合bind,但这些迭代无权访问先前迭代期间生成的值。

    (defn bind-each [k coll] (apply tcg/tuple (map (fn [x] (tcg/bind (tcg/return x) k)) coll))

  • 您可以使用原子(或挥发物)来存储和访问在先前迭代期间生成的值。这可行,但非常不符合 Clojure,特别是因为您需要reset!在返回生成器之前使用 atom/volatile,以避免它们的内容在生成器的下一次调用中被重用。

  • 生成器因其bindreturn函数而类似于 monad,这暗示了将诸如 Cats 之类的 monad 库与 State monad 结合使用。但是,State monad 在 Cats 2.0 中被删除(因为据称它不适合 Clojure),而我知道的其他支持库没有正式的 Clojurescript 支持。此外,在他自己的库中实现 State monad 时,Clojure 的 monad 专家之一 Jim Duey 似乎警告说 State monad 的使用与 test.check 的收缩不兼容(参见http://www. clojure.net/2015/09/11/Extending-Generative-Testing/),这大大降低了使用 test.check 的优点。

4

1 回答 1

2

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))))))
于 2016-10-31T13:45:24.277 回答