Как проверить функцию более высокого порядка с помощью QuickCheck?

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

gen :: a -> ([a] -> [a]) -> ([a] -> Bool) -> a

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

gen init next stop

где

init :: a
next :: [a] -> [a]
stop :: [a] -> Bool

Здесь свойство, которое я хотел бы проверить:

При любом вызове gen init next stop, gen promises никогда не передавать пустой список в next.

Могу ли я проверить это свойство с помощью QuickCheck, и если да, то как?

Ответ 1

Хотя это поможет, если вы дадите реализацию gen, я угадывая, что это происходит примерно так:

gen :: a -> ([a] -> [a]) -> ([a] -> Bool) -> a
gen init next stop = loop [init]
  where
    loop xs | stop xs   = head xs
            | otherwise = loop (next xs)

Свойство, которое вы хотите проверить, состоит в том, что next никогда не предоставляется пустой список. Препятствием для проверки этого является то, что вы хотите проверить инвариант внутреннего цикла внутри gen, поэтому это должно быть доступно из снаружи. Изменим gen, чтобы вернуть эту информацию:

genWitness :: a -> ([a] -> [a]) -> ([a] -> Bool) -> (a,[[a]])
genWitness init next stop = loop [init]
  where
    loop xs | stop xs   = (head xs,[xs])
            | otherwise = second (xs:) (loop (next xs))

Мы используем second из Control.Arrow. Оригинал gen легко определяется в терминах genWitness:

gen' :: a -> ([a] -> [a]) -> ([a] -> Bool) -> a
gen' init next stop = fst (genWitness init next stop)

Благодаря ленивой оценке это не даст нам больших накладных расходов. Вернуться к недвижимость! Чтобы включить показ сгенерированных функций из QuickCheck, мы используем модуль Test.QuickCheck.Function. Хотя здесь нет строгого требования, хорошая привычка состоит в том, чтобы monomorphise the property: мы используем списки Int вместо того, чтобы разрешать ограничение мономорфизма делает их единичными списками. Теперь давайте укажем свойство:

prop_gen :: Int -> (Fun [Int] [Int]) -> (Fun [Int] Bool) -> Bool
prop_gen init (Fun _ next) (Fun _ stop) =
    let trace = snd (genWitness init next stop)
    in  all (not . null) trace

Попробуем запустить его с помощью QuickCheck:

ghci> quickCheck prop_gen

Что-то похоже на цикл... Да, конечно: gen петли, если stop на списки из next никогда не True! Давайте вместо этого попытаемся взглянуть на конечные префиксы входной трассы вместо:

prop_gen_prefix :: Int -> (Fun [Int] [Int]) -> (Fun [Int] Bool) -> Int -> Bool
prop_gen_prefix init (Fun _ next) (Fun _ stop) prefix_length =
    let trace = snd (genWitness init next stop)
    in  all (not . null) (take prefix_length trace)

Теперь мы быстро получаем встречный пример:

385
{_->[]}
{_->False}
2

Вторая функция - это аргумент next, и если он возвращает пустой список, то цикл в gen даст next пустой список.

Я надеюсь, что это ответит на этот вопрос и что он дает вам некоторое представление как проверить функции более высокого порядка с помощью QuickCheck.

Ответ 2

Возможно, это плохой способ злоупотреблять этим, но QuickCheck не выполняет функцию, если она выдает исключение. Итак, чтобы проверить, просто дайте ему функцию, которая выдает исключение для пустого случая. Адаптация ответа danr:

import Test.QuickCheck
import Test.QuickCheck.Function
import Control.DeepSeq

prop_gen :: Int -> (Fun [Int] [Int]) -> (Fun [Int] Bool) -> Bool
prop_gen x (Fun _ next) (Fun _ stop) = gen x next' stop `deepseq` True
  where next' [] = undefined
        next' xs = next xs

Этот метод не требует изменения gen.