Циклы и управление состоянием в test.check

С введением Spec я пытаюсь написать генераторы test.check для всех моих функций. Это хорошо для простых структур данных, но имеет тенденцию становиться трудным со структурами данных, которые имеют части, которые зависят друг от друга. Другими словами, тогда требуется некоторое управление состоянием в генераторах.

Уже очень помогло бы иметь генератор-эквиваленты цикла / повторения или сокращения Clojure, так что значение, полученное в одной итерации, может быть сохранено в некотором агрегированном значении, которое затем будет доступно в следующей итерации.

Один простой пример, где это потребовалось бы, - написать генератор для разделения коллекции на ровно X разделов, где каждый раздел имеет от нуля до Y элементов, и где элементы затем случайным образом назначаются любому из разделов. (Обратите внимание, что test.chuck"s partition функция не позволяет указывать X или Y).

Если вы напишите этот генератор, просматривая коллекцию, то для этого потребуется доступ к разделам, заполненным во время предыдущих итераций, чтобы избежать превышения 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))

  • Вы можете использовать атомы (или летучие) для хранения и доступа к значениям, сгенерированным во время предыдущих итераций. Это работает, но очень не-Clojure, в частности, потому что вам нужно reset! атом / volatile перед возвращением генератора, чтобы избежать повторного использования их содержимого при следующем вызове генератора.

  • Генераторы похожи на монаду из-за их bind а также return функции, которые намекают на использование библиотеки монад, таких как Cats, в сочетании с государственной монадой. Тем не менее, монада State была удалена в Cats 2.0 (поскольку она якобы плохо подходила для Clojure), в то время как другие известные мне библиотеки поддержки не имеют формальной поддержки Clojurescript. Кроме того, при внедрении государственной монады в своей собственной библиотеке Джим Дуэй - один из экспертов-монад Clojure - похоже, предупреждает, что использование государственной монады несовместимо с сокращением test.check (см. Нижнюю часть http://www.clojure.net/2015/09/11/Extending-Generative-Testing/), что значительно снижает преимущества использования test.check.

1 ответ

Решение

Вы можете выполнить итерацию, которую вы описываете, комбинируя 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))))))
Другие вопросы по тегам