Невозможно определить пользовательский экземпляр `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)