4

Checkers is a library for reusable QuickCheck properties, particularly for standard type classes

How do I write a checkers instance to test whether my applicative instance of Validation is valid?

import Test.QuickCheck
import Test.QuickCheck.Checkers
import Test.QuickCheck.Classes
import Control.Applicative
import Data.Monoid

data Validation e a =
  Error e
  | Scss a
  deriving (Eq, Show)

instance Functor (Validation e) where
  fmap _ (Error e) = Error e
  fmap f (Scss a) = Scss $ f a

instance Monoid e => Applicative (Validation e) where
  pure = Scss
  (<*>) (Scss f) (Scss a) = Scss $ f a
  (<*>) (Error g) (Scss a) = Error g
  (<*>) (Scss a) (Error g) = Error g
  (<*>) (Error a) (Error g) = Error $ mappend a g

instance (Arbitrary a, Arbitrary b) => Arbitrary (Validation a b) where
  arbitrary = do
    a <- arbitrary
    b <- arbitrary
    elements [Scss a, Error b]

instance (Eq a, Eq b) => EqProp (Validation a b) where (=-=) = eq

main :: IO ()
main = quickBatch $ applicative [(Scss "b", Scss "a", Scss "c")]

I think I am almost there, but I get an error like:

chap17/Validation_applicative.hs:36:21: No instance for (CoArbitrary (Validation e0 [Char])) …
      arising from a use of ‘applicative’
    In the second argument of ‘($)’, namely
      ‘applicative [(Scss "b", Scss "a", Scss "c")]’
    In the expression:
      quickBatch $ applicative [(Scss "b", Scss "a", Scss "c")]
    In an equation for ‘main’:
        main = quickBatch $ applicative [(Scss "b", Scss "a", Scss "c")]
Compilation failed.

I have tried adding a CoArbitrary instance for Validation like so:

instance CoArbitrary (Validation a b)

but this leads to this error message:

chap17/Validation_applicative.hs:35:10: No instance for (GHC.Generics.Generic (Validation a b)) …
      arising from a use of ‘Test.QuickCheck.Arbitrary.$gdmcoarbitrary’
    In the expression: Test.QuickCheck.Arbitrary.$gdmcoarbitrary
    In an equation for ‘coarbitrary’:
        coarbitrary = Test.QuickCheck.Arbitrary.$gdmcoarbitrary
    In the instance declaration for ‘CoArbitrary (Validation a b)’
chap17/Validation_applicative.hs:38:21: No instance for (Eq e0) arising from a use of ‘applicative’ …
    The type variable ‘e0’ is ambiguous
    Note: there are several potential instances:
      instance Eq a => Eq (Const a b) -- Defined in ‘Control.Applicative’
      instance Eq a => Eq (ZipList a) -- Defined in ‘Control.Applicative’
      instance Eq a => Eq (Data.Complex.Complex a)
        -- Defined in ‘Data.Complex’
      ...plus 65 others
    In the second argument of ‘($)’, namely
      ‘applicative [(Scss "b", Scss "a", Scss "c")]’
    In the expression:
      quickBatch $ applicative [(Scss "b", Scss "a", Scss "c")]
    In an equation for ‘main’:
        main = quickBatch $ applicative [(Scss "b", Scss "a", Scss "c")]
Compilation failed.
The Unfun Cat
  • 29,987
  • 31
  • 114
  • 156

1 Answers1

9

How to

To automatically derive an instance of CoArbitrary, your data type should have an instance of Generic, which again can be automatically derived with some nice language extension:

{-# LANGUAGE DeriveGeneric #-}

import           GHC.Generics

data Validation e a =
  Error e
  | Scss a
  deriving (Eq, Show, Generic)

However the most significant mistake in your program is you were testing against [] but not your own type by applicative [(Scss "b", Scss "a", Scss "c")]. here's the definition of applicative test bundle, details omitted:

applicative :: forall m a b c.
               ( Applicative m
               , Arbitrary a, CoArbitrary a, Arbitrary b, Arbitrary (m a)
               , Arbitrary (m (b -> c)), Show (m (b -> c))
               , Arbitrary (m (a -> b)), Show (m (a -> b))
               , Show a, Show (m a)
               , EqProp (m a), EqProp (m b), EqProp (m c)
               ) =>
               m (a,b,c) -> TestBatch
applicative = const ( "applicative"
                    , [ ("identity"    , property identityP)
                      , ("composition" , property compositionP)
                      , ("homomorphism", property homomorphismP)
                      , ("interchange" , property interchangeP)
                      , ("functor"     , property functorP)
                      ]
                    )

In short, given four types m, a, b and c, this function will create a bunch of properties that m -- as an applicative functor -- should satisfy, and later you can test them with random a b c values generated by QuickCheck. [(Scss "b", Scss "a", Scss "c")] has type [(Validation String, Validation String, Validation String)] makes m ~ [].

So you should provide some value of type Validation e (a, b, c), or no value at all: You may have noticed the const right there in the definition of applicative, only the type of the argument matters:

main :: IO ()
main = quickBatch $ applicative (undefined :: Validation String (Int, Double, Char))

After that you may run the test and get well-formatted result. But no, you shouldn't test an applicative in this way.


Why should not to

The test provided by checkers is far from sufficient. By the runtime-monomorphic nature of GHC and how it treats ambiguity, you have to supply four concrete, non-polymorphic type to run the test like Validation String (Int, Double, Char), and the test module will generate and test against only those four types , while your applicative functor should work with any type that meets the context.

IMO most of the polymorphic functions do not fit well into a unit test framework: it cannot be tested against all possible types, so one has to choose either just do some test anyway with hand chosen types, or do the test on a type that's general enough (like Free monad when your code requires an arbitrary monad, but usually "general enough" is not well defined in other contexts).

You'd better rigorously examine your implementation and prove all the laws satisfied for all the cases, either with pen and paper or with some proving engine like agda. Here is an example on Maybe that may help: Proving Composition Law for Maybe Applicative


EDIT: please read the comment. I'm not entirely understanding it but it means Integer is the "general enough" type for unit testing polymorphic functions. I found This blog article by Bartosz Milewski and its bibliography good resources for grabbing the idea of parametricity and free theorem.

Community
  • 1
  • 1
zakyggaps
  • 3,070
  • 2
  • 15
  • 25
  • 3
    Parametricity means that often you don't need to test against all possible types. If you test your type for example against the monad laws applied to `Integer`, then you have proven the monad laws for all types, because you could embed any type into `Integer` (or any other type with infinite inhabitants) and any counter-example you had for another type would turn into a counter-example for `Integer`. – badcook Mar 15 '16 at 15:08
  • @badcook but some data type can have uncountable inhabitants while `Integer` is countable (maybe no difference in CS? I don't have a major in that field), also in haskell we have to deal with all the bottoms and lazy infinite lists which do not translate to `Integer` well I guess. – zakyggaps Mar 15 '16 at 15:32
  • 1
    If you're using it in a computer program it's definitely countable. Bottoms and laziness don't matter because the embedding I'm talking about doesn't have to preserve any structure (except be injective). Map each (computable) infinite list to an integer of your choice. – badcook Mar 15 '16 at 17:46
  • 2
    Practically speaking we might still want to test multiple types because we only generate a finite number of test cases and we would like to try to capture all edge cases in that finite number. `quickcheck`'s built in generators will probably do a better job of that than our theoretical embedding. Pretty obvious edge cases might no longer seem like edge cases if mapped to another type (e.g. maybe you map the empty string to 35). But testing just one type should already give you a lot of confidence. – badcook Mar 15 '16 at 17:51
  • 2
    Sorry for bringing back to the table an issue in a two year old answer, but "Why should not to" is over-prescriptive -- I wanted to link to your answer, but the second part discouraged me. badcook already addressed the matter of parametricity. On a more general note, there are good reasons to set up tests for class laws even if you know they can't replace actual proofs. Tests can be useful as a preliminary step to check whether an hypothesis is reasonable, as an early warning system if you ever accidentally introduce unlawfulness in your code, or simply as an illustration of the laws. – duplode Apr 05 '18 at 04:27