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