Невозможно определить пользовательский экземпляр `Arbitrary` для`Char`, так как он уже существует

Я попытался выполнить Введение в Quickcheck и хотел проверить свою функцию, которая принимает строки, содержащие цифры. Для этого я определил Arbitrary экземпляр для Char:

instance Arbitrary Char where
    arbitrary = choose ('0', '9')

Но GHC жалуется на это:

A.hs:16:10:
Duplicate instance declarations:
  instance Arbitrary Char -- Defined at A.hs:16:10
  instance [overlap ok] [safe] Arbitrary Char
    -- Defined in ‘Test.QuickCheck.Arbitrary’

Как я могу сказать ему забыть об уже определенном экземпляре и использовать мой собственный экземпляр? Или это вообще не сработает (что было бы странно, поскольку в учебном пособии используется такой подход)?

2 ответа

Решение

Как советовал @carsten-könig, решение было бы сделать newtype обертка для Char, Это не обходной путь, а правильный и действительно хороший способ избежать целого класса проблем, связанных с бесхозными экземплярами (экземплярами для классов типов, которые определены в другом модуле), подробнее о таких проблемах читайте здесь.

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

Например, рассмотрим Monoid класс типов, определенный в Data.Monoid как:

class Monoid a where
    mempty  :: a           -- ^ Identity of 'mappend'
    mappend :: a -> a -> a -- ^ An associative operation

Как вы, возможно, уже знаете, Monoid - это тип значений, которые можно добавлять друг к другу (используя mappend) и для которого существует значение "идентичности" mempty который удовлетворяет правилу mappend mempty a == a (добавление личности к значению a результаты в a). Существует очевидный пример Monoid для списков:

class Monoid [a] where
    mempty  = []
    mappend = (++)

Это также легко определить Int s. Действительно целые числа с операцией сложения образуют правильный моноид.

class Monoid Int where
    mempty  = 0
    mappend = (+)

Но является ли это единственно возможным моноидом для целых чисел? Конечно, это не так, умножение на целые числа сформировало бы еще один правильный моноид:

class Monoid Int where
    mempty  = 1
    mappend = (*)

Оба случая верны, но теперь у нас есть проблема: если вы попытаетесь оценить 1 `mappend` 2, нет никакого способа выяснить, какой экземпляр должен быть использован.

Вот почему Data.Monoid упаковывает экземпляры для чисел в упаковщики нового типа, а именно Sum а также Product,

Идем дальше, ваше заявление

instance Arbitrary Char where
    arbitrary = choose ('0', '9')

может быть очень запутанным. Он говорит: "Я произвольный символ", но выдает только цифры. На мой взгляд, это было бы намного лучше:

newtype DigitChar = DigitChar Char deriving (Eq, Show)

instance Arbitrary DigitChar where
    arbitrary = fmap DigitChar (choose ('0', '9'))

Кусок торта. Вы можете пойти дальше и спрятать DigitChar конструктор, обеспечивающий digitChar "умный конструктор", который не позволил бы создать DigitChar что на самом деле не цифра.

Что касается вашего вопроса "Знаете ли вы, почему это не тот подход, который использовался в учебнике?", Я думаю, что причина проста, учебник, кажется, написан в 2006 году, и в те дни quickcheck просто не определял Arbitrary экземпляр для Char, Таким образом, код в учебнике был совершенно верным в те времена.

Вам не нужно создавать новые Arbitrary экземпляры для генерации тестового ввода. Вы можете использовать QuickCheck's forAll комбинатор явно выбрать Gen a к функции:

digit :: Gen Char
digit = choose ('0', '9)

prop_myFun = forAll digit $ \x -> isAwesome (myFun x)
Другие вопросы по тегам