Как использовать SmallCheck в Haskell? - программирование
Подтвердить что ты не робот

Как использовать SmallCheck в Haskell?

Я пытаюсь использовать SmallCheck для проверки программы Haskell, но я не могу понять, как использовать библиотеку для тестирования собственных типы данных. По-видимому, мне нужно использовать Test.SmallCheck.Series. Тем не менее, я считаю, что документация очень запутанна. Меня интересуют как решения поваренной книги, так и понятное объяснение логической (монадической?) Структуры. Вот некоторые вопросы, которые у меня есть (все связанные):

  • Если у меня есть тип данных data Person = SnowWhite | Dwarf Integer, как объяснить smallCheck, что допустимые значения Dwarf 1 - Dwarf 7 (или SnowWhite)? Что делать, если у меня сложная структура данных FairyTale и конструктор makeTale :: [Person] -> FairyTale, и я хочу, чтобы smallCheck сделал FairyTale-s из списков Person-s с помощью конструктора?

    Мне удалось сделать quickCheck работать так, чтобы это не загрязняло мои руки, используя разумные приложения Control.Monad.liftM для таких функций, как makeTale. Я не мог найти способ сделать это с помощью smallCheck (пожалуйста, объясните мне это!).

  • Какова связь между типами Serial, Series и т.д.?

  • (необязательно) Какова точка coSeries? Как использовать тип Positive из SmallCheck.Series?

  • (необязательно) Любое разъяснение того, что является логикой того, что должно быть монадическим выражением, и что является просто регулярной функцией, в контексте smallCheck, будет оценено.

Если там есть интро/учебник по использованию smallCheck, я буду благодарен за ссылку. Большое вам спасибо!

ОБНОВЛЕНИЕ: Я должен добавить, что самая полезная и читаемая документация, которую я нашел для smallCheck, этот документ (PDF). На первый взгляд я не мог найти ответ на мои вопросы; это скорее убедительная реклама, чем учебник.

ОБНОВЛЕНИЕ 2: Я переместил свой вопрос о странном Identity, который отображается в типе Test.SmallCheck.list и других местах в отдельном вопросе.

4b9b3361

Ответ 1

ПРИМЕЧАНИЕ.. Этот ответ описывает версии SmallCheck до версии 1.0. См. это сообщение в блоге за важные отличия между SmallCheck 0.6 и 1.0.

SmallCheck похож на QuickCheck, поскольку он проверяет свойство над некоторой частью пространства возможных типов. Разница в том, что он пытается исчерпывающе перечислить серию всех "малых" значений вместо произвольного подмножества малых значений.

Как я и намекал, SmallCheck Serial похож на QuickCheck Arbitrary.

Теперь Serial довольно прост: тип Serial a имеет способ (series), чтобы сгенерировать тип series, который является просто функцией из Depth -> [a]. Или, чтобы распаковать это, объекты Serial являются объектами, мы знаем, как перечислять некоторые "маленькие" значения. Нам также присваивается параметр Depth, который определяет, сколько небольших значений мы должны сгенерировать, но не будем игнорировать его в течение минуты.

instance Serial Bool where series _ = [False, True]
instance Serial Char where series _ = "abcdefghijklmnopqrstuvwxyz"
instance Serial a => Serial (Maybe a) where
  series d = Nothing : map Just (series d)

В этих случаях мы делаем не что иное, как игнорирование параметра Depth, а затем перечисление "всех" возможных значений для каждого типа. Мы можем сделать это автоматически для некоторых типов

instance (Enum a, Bounded a) => Serial a where series _ = [minBound .. maxBound]

Это действительно простой способ тестирования свойств исчерпывающе - буквально проверить каждый возможный ввод! Очевидно, что есть хотя бы две основные проблемы: (1) бесконечные типы данных будут приводить к бесконечным циклам при тестировании и (2) вложенные типы приводят к экспоненциально большему количеству примеров для просмотра. В обоих случаях SmallCheck становится очень крупным очень быстро.

Так что точка параметра Depth - это позволяет системе просить нас сохранить наш series маленький. Из документации Depth находится

Максимальная глубина сгенерированных тестовых значений

Для значений данных это глубина вложенных конструкторских приложений.

Для функциональных значений это как глубина вложенного анализа случая, так и глубина результатов.

поэтому давайте переработаем наши примеры, чтобы они были маленькими.

instance Serial Bool where 
  series 0 = []
  series 1 = [False]
  series _ = [False, True]
instance Serial Char where 
  series d = take d "abcdefghijklmnopqrstuvwxyz"
instance Serial a => Serial (Maybe a) where
  -- we shrink d by one since we're adding Nothing
  series d = Nothing : map Just (series (d-1))

instance (Enum a, Bounded a) => Serial a where series d = take d [minBound .. maxBound]

Гораздо лучше.


Итак, что coseries? Как coarbitrary в классе Arbitrary QuickCheck, он позволяет нам создавать серию "маленьких" функций. Обратите внимание, что мы пишем экземпляр над типом ввода --- тип результата передается нам в другом аргументе Serial (который я ниже вызываю results).

instance Serial Bool where
  coseries results d = [\cond -> if cond then r1 else r2 | 
                        r1 <- results d
                        r2 <- results d]

это требует немного большей изобретательности для написания, и я действительно дам вам использовать методы alts, которые я опишу ниже.


Итак, как мы можем сделать некоторые series из Person s? Эта часть проста

instance Series Person where
  series           d = SnowWhite : take (d-1) (map Dwarf [1..7])
  ...

Но наша функция coseries должна генерировать каждую возможную функцию от Person до другого. Это можно сделать, используя ряд функций altsN, предоставляемых SmallCheck. Здесь один способ записать его

 coseries results d = [\person -> 
                         case person of
                           SnowWhite -> f 0
                           Dwarf n   -> f n
                       | f <- alts1 results d ]

Основная идея заключается в том, что altsN results генерирует series из N -ary функции из N значений с экземплярами Serial в экземпляр Serial results. Поэтому мы используем его для создания функции из [0..7], ранее определенного значения Serial, во все, что нам нужно, затем мы сопоставляем наш Person с числами и передаем их.


Итак, теперь, когда мы имеем экземпляр Serial для Person, мы можем использовать его для создания более сложных вложенных экземпляров Serial. Для "экземпляра", если FairyTale - это список Person s, мы можем использовать экземпляр Serial a => Serial [a] вместе с нашим экземпляром Serial Person, чтобы легко создать Serial FairyTale:

instance Serial FairyTale where
  series = map makeFairyTale . series
  coseries results = map (makeFairyTale .) . coseries results

((makeFairyTale .) составляет makeFairyTale с каждой функцией coseries, что немного запутывает)

Ответ 2

  • Если у меня есть тип данных data Person = SnowWhite | Dwarf Integer, как объяснить smallCheck, что допустимые значения Dwarf 1 - Dwarf 7 (или SnowWhite)?

Прежде всего, вам нужно решить, какие значения вы хотите генерировать для каждой глубины. Здесь нет единого правильного ответа, это зависит от того, насколько мелким вы хотите, чтобы ваше пространство поиска было.

Вот только два возможных варианта:

  • people d = SnowWhite : map Dwarf [1..7] (не зависит от глубины)
  • people d = take d $ SnowWhite : map Dwarf [1..7] (каждая единица глубины увеличивает пространство поиска на один элемент)

После того, как вы это решили, ваш экземпляр Serial прост, как

instance Serial m Person where
    series = generate people

Мы оставили здесь m полиморфным, поскольку нам не нужна какая-либо конкретная структура основной монады.

  • Что делать, если у меня сложная структура данных FairyTale и конструктор makeTale :: [Person] -> FairyTale, и я хочу, чтобы smallCheck сделал FairyTale-s из списков Person-s с помощью конструктора?

Используйте cons1:

instance Serial m FairyTale where
  series = cons1 makeTale
  • Какова связь между типами Serial, Series и т.д.?

Serial - тип типа; Series - это тип. Вы можете иметь несколько Series того же типа - они соответствуют различным способам перечисления значений этого типа. Однако может быть сложно указать для каждого значения, как он должен быть сгенерирован. Класс Serial позволяет указать хорошее значение по умолчанию для генерации значений определенного типа.

Определение Serial есть

class Monad m => Serial m a where
  series   :: Series m a

Таким образом, все, что он делает, назначает конкретную Series m a заданной комбинации m и a.

  • В чем смысл coseries?

Необходимо создать значения функциональных типов.

  • Как использовать тип Positive из SmallCheck.Series?

Например, например:

> smallCheck 10 $ \n -> n^3 >= (n :: Integer)
Failed test no. 5.
there exists -2 such that
  condition is false

> smallCheck 10 $ \(Positive n) -> n^3 >= (n :: Integer)
Completed 10 tests without failure.
  • Любое разъяснение того, что является логикой того, что должно быть монадическим выражением, и что является просто регулярной функцией в контексте smallCheck, было бы оценено.

Когда вы пишете экземпляр Serial (или любое выражение Series), вы работаете в монаде Series m.

Когда вы пишете тесты, вы работаете с простыми функциями, которые возвращают Bool или Property m.

Ответ 3

Хотя я думаю, что ответ @tel - отличное объяснение (и я желаю, чтобы smallCheck действительно работал так, как он описывает), код, который он предоставляет, не работает для меня (с smallCheck версия 1). Мне удалось заставить следующее работать...

ОБНОВЛЕНИЕ/ПРЕДУПРЕЖДЕНИЕ: Код ниже неверен для довольно тонкой причины. Для исправленной версии и подробностей см. этот ответ на вопрос, упомянутый ниже. Короче говоря, вместо instance Serial Identity Person нужно написать instance (Monad m) => Series m Person.

... но я нахожу использование Control.Monad.Identity, и все флаги компилятора странные, и я задал отдельный вопрос об этом.

Обратите внимание, что хотя Series Person (или фактически Series Identity Person) на самом деле не является точно таким же, как функции Depth -> [Person] (см. @tel answer), функция generate :: Depth -> [a] -> Series m a преобразуется между ними.

{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, FlexibleContexts, UndecidableInstances #-}
import Test.SmallCheck
import Test.SmallCheck.Series
import Control.Monad.Identity

data Person = SnowWhite | Dwarf Int

instance Serial Identity Person where
        series = generate (\d -> SnowWhite : take (d-1) (map Dwarf [1..7]))