2014-10-17 4 views
3

Я пытаюсь использовать HSpec и QuickCheck для проверки свойств моноидов (ассоциативность и элемент идентификации). Я собираюсь проверить конкретные экземпляры, но хотел бы сохранить большую часть кода полиморфным. Это то, что я придумал после нескольких часов:Используйте HSpec и QuickCheck для проверки Data.Monoid properties

module Test where 

import Test.Hspec 
import Test.QuickCheck 
import Data.Monoid 

instance (Arbitrary a) => Arbitrary (Sum a) where 
    arbitrary = fmap Sum arbitrary 

instance (Arbitrary a) => Arbitrary (Product a) where 
    arbitrary = fmap Product arbitrary 

prop_Monoid_mappend_mempty_x x = mappend mempty x === x 

sumMonoidSpec = it "mappend mempty x = x" $ property (prop_Monoid_mappend_mempty_x :: Sum Int -> Property) 
productMonoidSpec = it "mappend mempty x = x" $ property (prop_Monoid_mappend_mempty_x :: Product Double -> Property) 

main :: IO() 
main = hspec $ do 
    describe "Data.Monoid.Sum" $ do 
     sumMonoidSpec 
    describe "Data.Monoid.Product" $ do 
     productMonoidSpec 

То, что я хотел бы иметь хотя полиморфный

monoidSpec = it "mappend mempty x = x" $ property prop_Monoid_mappend_mempty_x 

и указать фактический моноидный экземпляр (сумму, произведение) и тип (Int , Double) позже. Проблема в том, что он не будет вводить проверку. Я постоянно получаю

src/[email protected]:42-18:50 No instance for (Arbitrary a0) arising from a use of property 
The type variable a0 is ambiguous 
Note: there are several potential instances: 
    instance Arbitrary a => Arbitrary (Product a) 
    -- Defined at /home/app/isolation-runner-work/projects/68426/session.207/src/src/Test.hs:10:10 
    instance Arbitrary a => Arbitrary (Sum a) 
    -- Defined at /home/app/isolation-runner-work/projects/68426/session.207/src/src/Test.hs:7:10 
    instance Arbitrary() -- Defined in Test.QuickCheck.Arbitrary 
    ...plus 27 others … 
src/[email protected]:51-18:79 No instance for (Monoid a0) 
    arising from a use of prop_Monoid_mappend_mempty_x 
The type variable a0 is ambiguous 
Note: there are several potential instances: 
    instance Monoid() -- Defined in Data.Monoid 
    instance (Monoid a, Monoid b) => Monoid (a, b) 
    -- Defined in Data.Monoid 
    instance (Monoid a, Monoid b, Monoid c) => Monoid (a, b, c) 
    -- Defined in Data.Monoid 
    ...plus 18 others … 

Я знаю, что нужно тягот моноидными в полиморфной версии быть Произвольное, Eq и Show, но я не знаю, как.

Вопрос в том, как выразить спецификации для моноида полиморфным способом и избежать дублирования кода?

+0

Возможно, вас заинтересует использование [hspec-законов] (https://github.com/hspec/hspec-laws#readme) или [hspec-checkers] (http://hackage.haskell.org/package/ hspec-checkers). –

ответ

3

Обратите внимание на тип property :: Testable prop => prop -> Property. Тип var prop стирается, и разрешение экземпляра не может иметь место, если переменная типа больше недоступна. В основном то, что вы хотите сделать, это отложить выделение, и для этого вы должны сделать доступный тип до момента выбора экземпляра.

Один из способов носить с собой дополнительный Proxy prop параметр:

-- Possibly Uuseful helper function 
propertyP :: Testable prop => Proxy prop -> prop -> Property 
propertyP _ = property 

monoidProp :: forall m . (Arbitrary m, Testable m, Show m, Monoid m, Eq m) 
      => Proxy m -> Property 
monoidProp _ = property (prop_Monoid_mappend_mempty_x :: m -> Property) 

monoidSpec :: (Monoid m, Arbitrary m, Testable m, Show m, Eq m) => Proxy m -> Spec 
monoidSpec x = it "mappend mempty x = x" $ monoidProp x 

main0 :: IO() 
main0 = hspec $ do 
    describe "Data.Monoid.Sum" $ do 
     monoidSpec (Proxy :: Proxy (Sum Int)) 
    describe "Data.Monoid.Product" $ do 
     monoidSpec (Proxy :: Proxy (Product Double)) 

Другой способ заключается в использовании библиотеки как tagged, которая обеспечивает тип Tagged, который просто добавляет некоторый параметр типа фантом к существующему типу:

import Data.Tagged 

type TaggedProp a = Tagged a Property 
type TaggedSpec a = Tagged a Spec 

monoidPropT :: forall a. (Monoid a, Arbitrary a, Show a, Eq a) 
      => TaggedProp a 
monoidPropT = Tagged (property (prop_Monoid_mappend_mempty_x :: a -> Property)) 

monoidSpecT :: forall a . (Monoid a, Arbitrary a, Show a, Eq a) => TaggedSpec a 
monoidSpecT = Tagged $ it "mappend mempty x = x" 
          (unTagged (monoidPropT :: TaggedProp a)) 

main1 :: IO() 
main1 = hspec $ do 
    describe "Data.Monoid.Sum" $ do 
     untag (monoidSpecT :: TaggedSpec (Sum Int)) 
    describe "Data.Monoid.Product" $ do 
     untag (monoidSpecT :: TaggedSpec (Product Double)) 

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

Для обоих из них требуется только -XScopedTypeVariables.

+0

Я немного играл с вашим решением и (исправьте меня, если я ошибаюсь), похоже, что на самом деле вам не требуется propertyP, поскольку он все равно игнорирует первый аргумент, и информация о типе уже присутствует. После вытаскивания свойстваP версия прокси выглядит почти так же, как Tagged - это просто вызывающий сайт, который выглядит немного лучше с Proxies. – maciekszajna

+0

Строго говоря, вам не нужно 'propertyP', но он обеспечивает тот факт, что 1-й и 2-й аргументы имеют один и тот же тип, что иногда может помочь вам написать явную подпись типа. – user2407038

Смежные вопросы