Как я могу протестировать функцию более высокого порядка с помощью 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 обещает никогда не передавать next пустой список.

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


person Norman Ramsey    schedule 13.03.2012    source источник
comment
Вот связанное свойство: для любого непустого ввода next будет производить непустой вывод. Возможно, вам будет интересно протестировать это свойство вместо или в дополнение к упомянутому вами свойству.   -  person John L    schedule 13.03.2012
comment
@JohnL Действительно так! Но это свойство next, а не gen, а next - это свойство первого порядка, поэтому я знаю, как это проверить.   -  person Norman Ramsey    schedule 15.03.2012


Ответы (2)


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

person danr    schedule 13.03.2012
comment
если вы изменили gen на gen :: Monad m => a -> ([a] -> m [a]) -> ([a] -> m Bool) -> m a, то вы можете поместить код свидетеля внутрь nextstop) и не беспокоиться о том, чтобы запятнать реализацию таким журналированием. - person rampion; 13.03.2012
comment
Вот это да. Я ожидал, что вокруг gen будет что-то вроде обертки, но я не уверен, что хочу, чтобы моя хорошая чистая gen была испорчена этим лишним мусором. Я изучу это и вернусь к вам. - person Norman Ramsey; 15.03.2012
comment
@NormanRamsey: с помощью предложения @ rampion, просто сделав его монадическим, а затем используя монаду писателя для трассировки, не потребуется слишком много мусора. Тогда next нужно только записать свой ввод в монаду записи. - person danr; 22.03.2012
comment
@NormanRamsey Я считаю, что сложность тестирования в том, что функция генерации не так уж и чиста. Думали ли вы, что первый аргумент будет списком, а не отдельным элементом? Это позволит НАМНОГО проще проверить это свойство, так как вы можете просто передать пустой список для проверки этого условия, вместо того, чтобы полагаться на поведение функций next и stop в вашем тесте. - person nightski; 22.03.2012
comment
@nightski: Как это поможет? Это немедленно нарушит собственность. Свойство, которое вы действительно хотите показать, заключается в том, что внутренний цикл, для которого я просто привел пример реализации выше, что next никогда не получает пустой список, и эти списки будут исходить из самого next и возможных комбинаций и перестановок изнутри цикла: вот что мы должны проверить. - person danr; 23.03.2012

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

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.

person Dan Burton    schedule 13.03.2012
comment
С этим есть одна проблема: если функция stop никогда не возвращает True, цикл будет бесконечен. Вместо функции QuickCheck пользователю необходимо создать stop, который гарантированно вернет True в какой-то момент. За исключением такой функции, как const True, это может быть трудно гарантировать. - person John L; 15.03.2012