Как вы пишете новый модификатор в QuickCheck - PullRequest
1 голос
/ 04 июня 2019

Я сталкивался с несколькими случаями в моем тестировании с QuickCheck, когда в некоторых случаях было бы проще написать свои собственные модификаторы, но я не совсем уверен, как это можно сделать. В частности, было бы полезно узнать, как написать модификатор для генераторов списков и чисел (например, Int). Я знаю о NonEmptyList, Positive и NonNegative, которые уже есть в библиотеке, но в некоторых случаях это сделало бы мои тесты более понятными, если бы я мог указать что-то вроде списка, который не только NonEmpty , но также и NonSingleton (то есть он имеет как минимум 2 элемента), или Int, который больше 1, не просто NonZero или Positive, или Int(egral), который является четным / нечетным и т. д.

1 Ответ

2 голосов
/ 04 июня 2019

Существует множество способов сделать это. Вот несколько примеров.

Функция комбинатора

Вы можете написать комбинатор как функцию. Вот тот, который генерирует не-одиночные списки из любого Gen a:

nonSingleton :: Gen a -> Gen [a]
nonSingleton g = do
  x1 <- g
  x2 <- g
  xs <- listOf g
  return $ x1 : x2 : xs

Этот тип имеет тот же тип, что и встроенная функция listOf, и может использоваться таким же образом:

useNonSingleton :: Gen Bool
useNonSingleton = do
  xs :: [String] <- nonSingleton arbitrary
  return $ length xs > 1

Здесь я воспользовался тем, что Gen a был Monad, так что я мог написать как функцию, так и свойство с пометкой do, но вы также можете написать ее, используя монадические комбинаторы, если вы так предпочитаете.

Функция просто генерирует два значения x1 и x2, а также список xs произвольного размера (который может быть пустым) и создает список всех трех. Поскольку x1 и x2 гарантированно будут одиночными значениями, результирующий список будет иметь по крайней мере эти два значения.

Фильтрация

Иногда вы просто хотите выбросить небольшое подмножество сгенерированных значений. Вы можете сделать это с помощью встроенного комбинатора ==>, который здесь используется непосредственно в свойстве:

moreThanOne :: (Ord a, Num a) => Positive a -> Property
moreThanOne (Positive i) = i > 1 ==> i > 1

Хотя это свойство является тавтологическим, оно демонстрирует, что предикат, который вы помещаете слева от ==>, гарантирует, что все, что выполняется с правой стороны ==>, пройдет предикат.

Существующие монадные комбинаторы

Поскольку Gen a является экземпляром Monad, вы также можете использовать существующие комбинаторы Monad, Applicative и Functor. Вот тот, который превращает любое число внутри любого Functor в четное число:

evenInt :: (Functor f, Num a) => f a -> f a
evenInt = fmap (* 2)

Обратите внимание, что это работает для любого Functor f, а не только для Gen a. Однако, поскольку Gen a является Functor, вы все равно можете использовать evenInt:

allIsEven :: Gen Bool
allIsEven = do
  i :: Integer <- evenInt arbitrary
  return $ even i

Вызов функции arbitrary здесь создает неограниченное значение Integer. evenInt затем делает это даже умножением на два.

Произвольные новинки

Вы также можете использовать newtype для создания собственных контейнеров данных, а затем сделать их Arbitrary экземплярами:

newtype Odd a = Odd a deriving (Eq, Ord, Show, Read)

instance (Arbitrary a, Num a) => Arbitrary (Odd a) where
  arbitrary = do
    i <- arbitrary
    return $ Odd $ i * 2 + 1

Это также позволяет вам реализовать shrink, если вам это нужно.

Вы можете использовать newtype в таком свойстве:

allIsOdd :: Integral a => Odd a -> Bool
allIsOdd (Odd i) = odd i

Экземпляр Arbitrary использует arbitrary для типа a, чтобы сгенерировать неограниченное значение i, затем удваивает его и добавляет единицу, тем самым гарантируя, что значение является нечетным.

Посмотрите документацию QuickCheck , чтобы узнать о многих других встроенных комбинаторах. Я особенно нахожу choose, elements, oneof и suchThat полезными для выражения дополнительных ограничений.

...