QuickCheck: Type-directed Property Testing

> module Testing where
> import Test.QuickCheck
> import Data.Char
> import Data.List (sort, nubBy)

In this lecture, we will look at QuickCheck, a technique that cleverly exploits typeclasses and monads to deliver a powerful automatic testing methodology.

Quickcheck was developed by Koen Claessen and John Hughes at 2000 and ten years later won the most influential paper award. This was expected, as since then it has been ported to other languages and is currently used, among other things to find subtle concurrency bugs in telecommunications code.

The key idea on which QuickCheck is founded, is property-based testing. That is, instead of writing individual test cases (e.g., unit tests corresponding to input-output pairs for particular functions) one should write properties that are desired of the functions, and then automatically generate random tests which can be run to verify (or rather, falsify) the property.

By emphasizing the importance of specifications, QuickCheck yields several benefits:

  1. The developer is forced to think about what the code should do,

  2. The tool finds corner-cases where the specification is violated, which leads to either the code or the specification getting fixed,

  3. The specifications live on as rich, machine-checkable documentation about how the code should behave.

Properties

A QuickCheck property is essentially a function whose output is a boolean. The standard “hello-world” QC property is

> prop_revapp :: [Int] -> [Int] -> Bool
> prop_revapp xs ys = 
>   reverse (xs ++ ys) == reverse xs ++ reverse ys

That is, a property looks a bit like a mathematical theorem that the programmer believes is true. A QC convention is to use the prefix ‘prop_’ for QC properties. Note that the type signature for the property is not the usual polymorphic signature; we have given the concrete type Int for the elements of the list. This is because QC uses the types to generate random inputs, and hence is restricted to monomorphic properties (that don’t contain type variables.)

To check a property, we simply invoke the function

quickCheck :: (Testable prop) => prop -> IO ()
    -- Defined in Test.QuickCheck.Test

lets try it on our example property above

ghci> quickCheck prop_revapp
*** Failed! Falsifiable (after 2 tests and 1 shrink):
[0]
[1]

Whats that ?! Well, lets run the property function on the two inputs

ghci> prop_revapp [0] [1]
False

QC has found a sample input for which the property function fails i.e., returns False. Of course, those of you who are paying attention will realize there was a bug in our property, namely it should be

> prop_revapp_ok :: [Int] -> [Int] -> Bool
> prop_revapp_ok xs ys = 
>   reverse (xs ++ ys) == reverse ys ++ reverse xs

because reverse will flip the order of the two parts xs and ys of xs ++ ys. Now, when we run

ghci> quickCheck prop_revapp_ok
+++ OK, passed 100 tests.

That is, Haskell generated 100 test inputs and for all of those, the property held. You can up the stakes a bit by changing the number of tests you want to run

> quickCheckN   :: (Testable p) => Int -> p -> IO ()
> quickCheckN n = quickCheckWith $ stdArgs { maxSuccess = n }

and then do

ghci> quickCheckN 10000 prop_revapp_ok
+++ OK, passed 10000 tests.

QuickCheck Fancy Sort

Lets look at a slightly more interesting example. Remember our fancy sorting algorithm?

> fsort :: (Ord a) => [a] -> [a]
> fsort = mergeAll . sequences
> 
> sequences (a:b:xs)
>   | a > b     = descending b [a]  xs
>   | otherwise = ascending  b (a:) xs
> sequences xs  = [xs]
> 
> descending a as (b:bs)
>   | a > b          = descending b (a:as) bs
> descending a as bs = (a:as): sequences bs
> 
> ascending a as (b:bs)
>   | a <= b        = ascending b (\ys -> as (a:ys)) bs
> ascending a as bs = as [a]: sequences bs
> 
> mergeAll [x] = x
> mergeAll xs  = mergeAll (mergePairs xs)
> 
> mergePairs (a:b:xs) = merge a b: mergePairs xs
> mergePairs xs       = xs
> 
> merge as@(a:as') bs@(b:bs')
>   | a > b           = b:merge as  bs'
>   | a < b           = a:merge as' bs
>   | otherwise       = a:merge as' bs'
> merge [] bs         = bs
> merge as []         = as

Lets run it “by hand” on a few inputs

ghci> [10,9..1]
[10,9,8,7,6,5,4,3,2,1]
ghci> fsort [10,9..1]
[1,2,3,4,5,6,7,8,9,10]
ghci> [2,4..20] ++ [1,3..11]
[2,4,6,8,10,12,14,16,18,20,1,3,5,7,9,11]
ghci> fsort $ [2,4..20] ++ [1,3..11]
[1,2,3,4,5,6,7,8,9,10,11,12,14,16,18,20]

Looks good – lets try to test that the output is in fact sorted. We need a function that checks that a list is ordered

> isOrdered ::         (Ord a) => [a] -> Bool
> isOrdered (x1:x2:xs) = x1 <= x2 && isOrdered (x2:xs)
> isOrdered _          = True

and then we can use the above to write a property

> prop_fsort_isOrdered :: [Int] -> Bool
> prop_fsort_isOrdered xs = isOrdered (fsort xs)

and use Quickcheck to test it

ghci> quickCheckN 10000 isOrdered
+++ OK, passed 10000 tests.

Conditional Properties

Here are several other properties that we might want. First, repeated fsorting should not change the list. That is,

> prop_fsort_idemp :: [Int] -> Bool
> prop_fsort_idemp xs = fsort (fsort xs) == fsort xs

Second, the head of the result is the minimum element of the input

> prop_fsort_min :: [Int] -> Bool
> -- prop_fsort_min [] = True 
> prop_fsort_min xs = null xs ||  head (fsort xs) == minimum xs

However, when we run this, we run into a glitch

ghci> quickCheck prop_fsort_min
*** Failed! Exception: 'Prelude.head: empty list' (after 1 test):
[]

Q: Can you modify the property prop_fsort_min to get rid of the runtime exception?

But of course! The earlier properties held for all inputs while this property makes no sense if the input list is empty! This is why thinking about specifications and properties has the benefit of clarifying the preconditions under which a given piece of code is supposed to work.

In this case we want a conditional properties where we only want the output to satisfy to satisfy the spec if the input meets the precondition that it is non-empty.

> prop_fsort_nn_min    :: [Int] -> Property
> prop_fsort_nn_min xs =
>   not (null xs) ==> head (fsort xs) == minimum xs
> prop_Eq :: Int -> Int -> Property 
> prop_Eq x y = (show (x) ++ "Foo") === (show y)
> 
> prop_fsort_nn_max    :: [Int] -> Property
> prop_fsort_nn_max xs =
>   not (null xs) ==> last (fsort xs) == maximum xs

This time around, both the property holds!

ghci> quickCheckN 1000 prop_fsort_nn_min
+++ OK, passed 1000 tests.

Q: Can you write a similar property for the maximum of the list?

Note that now, instead of just being a Bool the output of the function is a Property a special type built into the QC library. Similarly the implies combinator ==> is on of many QC combinators that allow the construction of rich properties.

(==>) :: Testable prop => Bool -> prop -> Property

Testing against a model implementation

We could keep writing different properties that capture various aspects of the desired functionality of fsort. Another approach for validation is to test that our fsort is behaviourally identical to a trusted reference implementation which itself may be too inefficient or otherwise unsuitable for deployment. In this case, lets use the standard library’s sort function

> prop_fsort_sort    :: [Int] -> Bool
> prop_fsort_sort xs =  fsort xs == sort xs

which we can put to the test

ghci> quickCheckN 1000 prop_fsort_sort
*** Failed! Falsifiable (after 9 tests and 6 shrink):
[1,0,0]

Say, what?!

ghci> fsort [1,0,0]
[0,1]

Ugh! So close, and yet … Can you spot the bug in our code?

Aha! Merging is implemented to eliminate the duplicates!

Is this a bug in the code? What is a bug anyway? Perhaps the fact that the duplicates are eliminated is a feature! At any rate there is an inconsistency between our mental model of how the code should behave as articulated in prop_fsort_sort and the actual behavior of the code itself.

We can rectify matters by stipulating that the fsort is only called on lists of distinct elements

> isDistinct ::(Eq a) => [a] -> Bool
> isDistinct (x:xs) = not (x `elem` xs) && isDistinct xs
> isDistinct _      = True

and then, weakening the equivalence to only hold on inputs that are duplicate-free

> prop_fsort_distinct_sort :: [Int] -> Property
> prop_fsort_distinct_sort xs =
>   (isDistinct xs) ==> (fsort xs == sort xs)

QuickCheck happily checks the modified property

ghci> quickCheck prop_fsort_distinct_sort
+++ OK, passed 100 tests.

The perils of Conditional Testing

Well, we managed to fix the fsort property, but beware! Adding preconditions leads one down a slippery slope. In fact, if we paid closer attention to the above runs, we would notice something

ghci> quickCheckN 10000 prop_fsort_distinct_sort
...
(5012 tests; 248 discarded)
...
+++ OK, passed 10000 tests.

The bit about some tests being discarded is ominous. In effect, when the property is constructed with the ==> combinator, QC discards the randomly generated tests on which the precondition is false. In the above case QC grinds away on the remainder until it can meet its target of 10000 valid tests. This is because the probability of a randomly generated list meeting the precondition (having distinct elements) is high enough. This may not always be the case.

The following code is (a simplified version of) the insert function from the standard library

> insert x []                 = [x]
> insert x (y:ys) | x < y     = x : y : ys
>                 | otherwise = y : insert x ys

Given an element x and a list xs, the function walks along xs till it finds the first element greater than x and it places x to the left of that element. Thus

ghci> insert 8 ([1..3] ++ [10..13])
[1,2,3,8,10,11,12,13]

Indeed, the following is the well known insertion-sort algorithm

> isort :: (Ord a) => [a] -> [a]
> isort xs = foldr insert [] xs

We could write our own tests, but why do something a machine can do better?!

> prop_isort_sort    :: [Int] -> Bool
> prop_isort_sort xs = isort xs == sort xs
ghci> quickCheckN 10000 prop_isort_sort
+++ OK, passed 10000 tests.

Now, the reason that the above works is that the insert routine preserves sorted-ness. That is while of course the property

> prop_insert_ordered'      :: Int -> [Int] -> Bool
> prop_insert_ordered' x xs = isOrdered (insert x xs)

is bogus

ghci> quickCheckN 10000 prop_insert_ordered'
*** Failed! Falsifiable (after 4 tests and 1 shrink):
0
[0,-1]
ghci> insert 0 [0, -1]
[0, -1, 0]

the output is ordered if the input was ordered to begin with

> prop_insert_ordered      :: Int -> [Int] -> Property
> prop_insert_ordered x xs =
>   isOrdered xs ==> isOrdered (insert x xs)

Notice that now, the precondition is more complex – the property requires that the input list be ordered. If we QC the property

ghci> quickCheckN 10000 prop_insert_ordered
*** Gave up! Passed only 5409 tests.

Ugh! The ordered lists are so sparsely distributed among random lists, that QC timed out well before it found 10000 valid inputs!

Aside the above example also illustrates the benefit of writing the property as p ==> q instead of using the boolean operator (||) to write not p || q. In the latter case, there is a flat predicate, and QC doesn’t know what the precondition is, so a property may hold vacuously. For example consider the variant

> prop_insert_ordered_vacuous :: Int -> [Int] -> Bool
> prop_insert_ordered_vacuous x xs =
>   not (isOrdered xs) || isOrdered (insert x xs)

QC will happily check it for us

ghci> quickCheckN 1000 prop_insert_ordered_vacuous
+++ OK, passed 10000 tests.

Unfortunately, in the above, the tests passed vacuously only because their inputs were not ordered, and one should use (==>) to avoid the false sense of security delivered by vacuity.

QC provides us with some combinators for guarding against vacuity by allowing us to investigate the distribution of test cases

collect  :: Show a => a -> Property -> Property
classify :: Bool -> String -> Property -> Property

We may use these to write a property that looks like

> prop_insert_ordered_vacuous' :: Int -> [Int] -> Property
> prop_insert_ordered_vacuous' x xs =
>   collect (length xs) $
>   classify (isOrdered xs) "ord" $
>   classify (not (isOrdered xs)) "not-ord" $
>   not (isOrdered xs) || isOrdered (insert x xs)

When we run this, as before we get a detailed breakdown of the 100 passing tests

ghci> quickCheck prop_insert_ordered_vacuous'
+++ OK, passed 100 tests:
 8% 5, not-ord
 5% 3, not-ord
 3% 0, ord
 5% 2, ord
 5% 12, not-ord
 5% 10, not-ord
 2% 1, ord
 4% 18, not-ord
 3% 6, not-ord9% 1, ord
...

where a line P% N, COND means that p percent of the inputs had length N and satisfied the predicate denoted by the string COND. Thus, as we see from the above, a paltry 14% of the tests were ordered and that was because they were either empty (3% 0, ord) or had one (2% 1, ord) or two elements (1% 2, ord). The odds of randomly stumbling upon a beefy list that is ordered are rather small indeed!

Generating Data

Before we start discussing how QC generates data (and how we can help it generate data meeting some pre-conditions), we must ask ourselves a basic question: how does QC behave randomly in the first place?!

ghci> quickCheck prop_insert_ordered'
*** Failed! Falsifiable (after 4 tests and 2 shrinks):
0
[0,-1]
ghci> quickCheck prop_insert_ordered'
*** Failed! Falsifiable (after 5 tests and 5 shrinks):
0
[1,0]

Eh? This seems most impure – same inputs yielding two totally different outputs! Well, this should give you a clue as to one of the key techniques underlying QC – monads!

The Generator Monad

A Haskell term that generates a (random value) of type a has the type Gen a which is defined as

newtype Gen a = MkGen { unGen :: StdGen -> Int -> a }

In effect, the term is a function that takes as input a random number generator StdGen and a seed Int and returns an a value. One can easily (and we shall see, profitably!) turn Gen into a Monad by

instance Monad Gen where
 -- return :: a -> Gen a 
    return x =
      MkGen (\_ _ -> x)

 -- (>>=) :: Gen a -> (a -> Gen b) -> Gen b
    MkGen a >>= f =
      MkGen (\r n ->
        let (r1, r2)  = split r
           MkGen b = f (a r1 n)
        in b r2 n
      )

The function split simply forks the random number generator into two parts; which are used by the left and right parameters of the bind operator (>>=). (Aside you should be able to readily spot the similarity between random number generators and the ST monad – in both cases the basic action is to grab some value and transition the state to the next-value.)

The Arbitrary Typeclass

QC uses the above to define a typeclass for types for which random values can be generated!

class Show a where
  show :: a -> String
class Arbitrary a where
  arbitrary :: Gen a

Thus, to have QC work with (i.e., generate random tests for) values of type a we need only make a an instance of Arbitrary by defining an appropriate arbitrary function for it. QC defines instances for base types like Int , Float, lists etc. and lifts them to compound types much like we did for JSON a few lectures back.

instance (Arbitrary a, Arbitrary b) => Arbitrary (a,b) where
  arbitrary = do x <- arbitrary
                 y <- arbitrary
                 return (x,y)

or more simply

instance (Arbitrary a, Arbitrary b) => Arbitrary (a,b) where
  arbitrary = (,) <$> arbitrary <*> arbitrary

Generator Combinators

QC comes loaded with a set of combinators that allow us to create custom instances for our own types.

The first of these combinators is choose

choose :: (System.Random.Random a) => (a, a) -> Gen a

which takes an interval and returns an random element from that interval. (The typeclass System.Random.Random describes types which can be sampled. For example, the following is a randomly chosen set of numbers between 0 and 3.

ghci> sample $ choose (0, 3)
2
2
2
1
2
1
0
2
2
1
3

Q: What is a plausible type for sample?

Show a => Gen a ->     [a]  -- 1)
          Gen a -> Gen [a]  -- 2)
Show a => Gen a -> IO  [a]  -- 3)
Show a => Gen a -> IO  ()   -- 4)
          Gen a -> IO  ()   -- 5)
          Gen a -> IO   a   -- 6)

A second useful combinator is elements

elements :: [a] -> Gen a

which returns a generator that produces values drawn from the input list

ghci> sample $ elements [10, 20..100]
60
70
30
50
30
20
20
10
100
80
10

Q: Lets try to figure out the implementation of elements

elements :: [a] -> Gen a
elements = error "Define me!"
> elems :: [a] -> Gen a 
> elems xs = do 
>   i <- choose (0, length xs-1) -- :: (a, a) -> Gen a 
>   return $ xs!!i
> 
> 
> myoneOf :: [Gen a] -> Gen a 
> myoneOf xs = do 
>   i <- choose (0, length xs-1)  
>   xs!!i

A third combinator is oneof

oneof :: [Gen a] -> Gen a

which allows us to randomly choose between multiple generators

ghci> sample $ oneof [elements [10,20,30], choose (0,3)]
10
0
10
1
30
1
20
2
20
3
30

Q: Lets try to figure out the implementation of oneOf

oneOf :: [Gen a] -> Gen a
oneOf = error "Define me!"

Finally, oneOf is generalized into the frequency combinator

frequency :: [(Int, Gen a)] -> Gen a

which allows us to build weighted combinations of individual generators.

Generating Ordered Lists

We can use the above combinators to write generators for lists

> genList1 ::  (Arbitrary a) => Gen [a]
> genList1 = do 
>   x  <- arbitrary 
>   xs <- genList1
>   return (x:xs)
> 
> genList ::  (Arbitrary a) => Gen [a]
> genList = myoneOf [return [] , (:) <$> arbitrary <*> genList]
> 
> genList' ::  (Arbitrary a) => Gen [a]
> genList' = frequency [(1, return []) , (100,genList)]

Let’s sample it!

sample genList1 

Can you spot a problem in the above?

Problem: genList1 only generates infinite lists! Hmm. Lets try again,

> genList2 ::  (Arbitrary a) => Gen [a]
> genList2 = oneof [ return []
>                  , (:) <$> arbitrary <*> genList2]

This is not bad, but we may want to give the generator a higher chance of not finishing off with the empty list, so lets use

> genList3 ::  (Arbitrary a) => Gen [a]
> genList3 = frequency [ (1, return [])
>                      , (7, (:) <$> arbitrary <*> genList2) ]

We can use the above to build a custom generator that always returns ordered lists by piping the generate list into the sort function

> genOrdList :: (Ord a, Arbitrary a) => Gen [a]
> genOrdList = sort <$> genList3

To check the output of a custom generator we can use the forAll combinator

forAll :: (Show a, Testable prop) 
       => Gen a -> (a -> prop) -> Property

For example, we can check that in fact, the combinator only produces ordered lists

ghci> quickCheckN 1000 $ forAll genOrdList isOrdered
+++ OK, passed 1000 tests.

and now, we can properly test the insert property

> prop_insert :: Int -> Property
> prop_insert x = forAll genOrdList $ 
>   \xs -> -- collect (length xs) $ 
>          isOrdered xs && isOrdered (insert x xs)
ghci> quickCheckN 1000 prop_insert
+++ OK, passed 1000 tests.

Note how the frequency of the empty lists fails when we modify the generator frequency.

Using Quickeck to Grade your Homeworks

Ben has been using QC to grade your homeworks. Here are few interesting examples.

Functional Equivalence: Primality Testing

To test your isPrime function (HW1, Problem 2) we defined a model implementation isPrimeOK

> factors :: Int -> [Int] 
> factors n = [i | i <- [1..n], n `mod` i == 0]
> 
> isPrimeOK :: Int -> Bool 
> isPrimeOK n = factors n == [1,n]

and tested the model implementation isPrimeOK against
your submited function, say isPrimeSol

> isPrimeSol:: Int -> Bool 
> isPrimeSol n = (n < 0) || length (factors n) == 2

There are many ways to compares these two functions. For example, a bad comparison will always return True

> prop_isPrime_bad :: Int -> Bool
> prop_isPrime_bad _ = True

As another bad check, we check primality on all integers, even the negative ones!

> prop_isPrime_bad_neg :: Int -> Bool
> prop_isPrime_bad_neg n = 
>   isPrimeOK n == isPrimeSol n

Let’s naively restrict checking to positive numbers:

> prop_isPrime_naive :: Int -> Bool
> prop_isPrime_naive n = 
>   (n <= 0) || isPrimeOK n == isPrimeSol n

We saw that QC behaves more precisely when we use (==>) to condition testing

> prop_isPrime :: Int -> Property
> prop_isPrime n = 
>   (n > 0) ==> isPrimeOK n == isPrimeSol n

Yet again, we need to be very cautious when using (==>) so that we do not exclude meaningful tests:

> prop_isPrime_bad_asm :: Int -> Property
> prop_isPrime_bad_asm n = 
>   (isPrimeSol n) ==> isPrimeOK n == isPrimeSol n

Or get very restrictive with out assumptions

> prop_isPrime_bad_timeout :: Int -> Property
> prop_isPrime_bad_timeout n = 
>   (n == 5) ==> isPrimeOK n == isPrimeSol n

Finally, it is always better to use QC’s building constructs to consruct our tests

> prop_isPrime_best :: (Positive Int) -> Bool
> prop_isPrime_best (Positive n) = 
>   isPrimeOK n == isPrimeSol n

User Defined Aribitrary Instances: The balkans

Recall the HW1, problem 3, the graph coloring problem.

> data Balkan = Albania | Bulgaria | BosniaAndHerzegovina
>             | Kosovo | Macedonia | Montenegro
>   deriving (Show, Eq)
> 
> data Color = Blue | Red | Green | Yellow
>   deriving (Show, Eq)
> 
> balkans :: [Balkan]
> balkans = [Albania, Bulgaria, BosniaAndHerzegovina, Kosovo, Macedonia, Montenegro]
> 
> colors :: [Color]
> colors = [Blue, Red, Green, Yellow]

Recall the isGoodColoring function, which, given a list of adjacent Balkans and a coloring of those Balkans, returns true if the coloring was good and false otherwise. Here are two solutions, the student solution “_stud" and the teacher solution “_teach."

> isGoodColoring_teach :: [(Balkan, Balkan)] -> [(Balkan,Color)] -> Bool 
> isGoodColoring_teach adj coloring 
>   = null [ (c1,c2) | (c1,c2) <- adj, lookup c1 coloring == lookup c2 coloring && lookup c1 coloring /= Nothing]
> 
> isGoodColoring_stud :: [(Balkan, Balkan)] -> [(Balkan,Color)] -> Bool 
> isGoodColoring_stud adj coloring = null $ do
>   (c1, c2) <- adj
>   if ((lookup c1 coloring)==(lookup c2 coloring))&& (lookup c1 coloring /= Nothing)
>     then
>     return (c1, c2)
>     else
>     []

In order to grade your isGoodColoring function, we first defined simple Arbitrary instances for both Balkan and Color using elements.

> instance Arbitrary Balkan where
>   arbitrary = elements balkans
> 
> instance Arbitrary Color where
>   arbitrary = elements colors

Here is a naive test using only these arbitrary instances:

> testColoringNaive :: [(Balkan, Balkan)] -> [(Balkan, Color)] -> Bool
> testColoringNaive adj col =
>   isGoodColoring_teach adj col == isGoodColoring_stud adj col

Why might this test be bad?

First, we would like to guarentee that the tested coloring is “complete”:

> sameElems :: Eq a => [a] -> [a] -> Bool 
> sameElems xs ys =  length xs == length ys && all (`elem` ys) xs
> 
> complete :: [(Balkan, Color)] -> Bool
> complete coloring = balkans `sameElems` col_balks
>   where
>     col_balks = map fst coloring
> 
> testColoringComplete :: [(Balkan, Balkan)] -> [(Balkan, Color)] -> Property
> testColoringComplete adj col =
>   complete col ==>
>   isGoodColoring_teach adj col == isGoodColoring_stud adj col

This is better, but now how do we actually generate only complete colorings?

> genRandCompleteColoring = do
>   c1 <- arbitrary
>   c2 <- arbitrary
>   c3 <- arbitrary
>   c4 <- arbitrary
>   c5 <- arbitrary
>   c6 <- arbitrary  
>   return [(Albania, c1), (Bulgaria, c2), (BosniaAndHerzegovina, c3), (Kosovo, c4),
>           (Macedonia, c5), (Montenegro, c6)]

Finally, let’s guarentee that there is no repetition in the adjacency list.

> genRandAdjacencies :: Gen [(Balkan, Balkan)]
> genRandAdjacencies = do
>   adj <- arbitrary
>   return $ nubBy (\(x1,y1) (x2,y2) -> (x1,y1) == (x2,y2) || (x1,y1) == (y2, x2)) adj

So now we finally have:

> testColoring :: Property
> testColoring =
>   forAll genRandCompleteColoring $ \coloring ->
>   forAll genRandAdjacencies $ \adj ->
>   complete coloring ==>
>   isGoodColoring_teach adj coloring == isGoodColoring_stud adj coloring

User Defined Aribitrary Instances: Binary Trees

Recall our binary trees from HW2, problem 1.

> data Tree a = Tip | Bin a (Tree a) (Tree a)
>   deriving (Eq, Show)

We had you define map over trees, as follows:

> map_stud :: (a -> b) -> Tree a -> Tree b 
> map_stud _ Tip = Tip 
> map_stud f (Bin x l r) = Bin (f x) (map_stud f l) (map_stud f r)
> 
> map_teach :: (a -> b) -> Tree a -> Tree b 
> map_teach _ Tip = Tip 
> map_teach f (Bin x l r) = Bin (f x) (map_teach f l) (map_teach f r) 

How can we create arbitrary Binary trees to pass to map?

Let’s try to model this after genList1/2/3 from earlier.

> genArbTree :: (Arbitrary a) => Gen (Tree a)
> genArbTree = frequency [ (1, return Tip)
>                        , (7, genArbBin)]

This returns either empty or an arbitrary, non-empty Binary Tree. What does that look like?

> genArbBin :: (Arbitrary a) => Gen (Tree a)
> genArbBin = do
>   v <- arbitrary
>   t1 <- genArbTree
>   t2 <- genArbTree
>   return $ Bin v t1 t2

Let’s define a test for this:

> intFuns :: [Int -> Int]
> intFuns = [(+1), (*42),(^3), ((+4) . (*3))]
> 
> testMap1 :: Property
> testMap1 =
>   forAll genArbTree $
>   \t ->
>     all (\f -> map_teach f t == map_stud f t) intFuns

This looks pretty good… What problems might arise?

Let’s try a slightly modified approach:

> genArbTree2 :: (Arbitrary a) => Gen (Tree a)
> genArbTree2 = oneof [ return Tip
>                     , Bin <$> arbitrary <*> genArbTree2 <*> genArbTree2 ]
> 
> genArbTree3 :: (Arbitrary a) => Gen (Tree a)
> genArbTree3 = frequency [ (1, return Tip)
>                         , (3, Bin <$> arbitrary <*> genArbTree2 <*> genArbTree2)]
> 
> testMap2 :: Property
> testMap2 =
>   forAll genArbTree3 $
>   \t ->
>     all (\f -> map_stud f t == map_teach f t) intFuns

A final, alternative approach:

> instance (Arbitrary a) => Arbitrary (Tree a) where
>   arbitrary = sized arbTree
> 
> arbTree :: Arbitrary a => Int -> Gen (Tree a)
> arbTree 0 = do
>   return Tip
> arbTree n = do
>   (Positive m1) <- arbitrary
>   (Positive m2) <- arbitrary
>   let n1  = n `div` (m1+1)
>   let n2 = n `div` (m2+1)
>   t1 <- (arbTree n1)
>   t2 <- (arbTree n2)
>   a <- arbitrary
>   return $ Bin a t1 t2
> 
> testMap3 :: Tree Int -> Bool
> testMap3 t =
>     all (\f -> map_stud f t == map_teach f t) intFuns