Further homage to QuickCheck

Lets suppose you’ve got two similar recursive types that differ only in their “payload”. For example, you want a binary tree of boolean operations to store a Char and another similar tree to store a set of Char. Let’s call them Tree and STree. Something like in the following, non-working, code

data Tree = And Tree Tree
  | Or Tree Tree
  | Leaf Char

data STree = And STree STree
  | Or STree STree
  | Leaf (Set Char)

We’re repeating a lot of boiler plate here. It’d be great if I could parameterise the tree, which I can.

data MTree t = And (MTree t) (MTree t)
  | Or (MTree t) (MTree t)
  | Leaf t

This is great, it removes boilerplate, and gives me less code to test.

Take our Tree as defined above. It’s easy to create an Arbitrary Tree. The following code creates an arbitrary tree with a maximum branch depth (generally for efficiency reasons you don’t want test trees that are either (arbitrary :: Int) or infinitely deep)

import Test.QuickCheck
import Control.Monad

data Tree = And Tree Tree
  | Or Tree Tree
  | Leaf Char
  deriving Show

arbitraryTree :: Int -> Gen Tree
arbitraryTree 0        =
  do
    c <- arbitrary
    return (Leaf c)
arbitraryTree maxDepth =
  do
    t <- oneof [arbitraryTree 0,
                liftM2 And (arbitraryTree depth') (arbitraryTree depth'),
                liftM2 Or (arbitraryTree depth') (arbitraryTree depth')]
    return t
  where
    depth' = maxDepth -1

instance Arbitrary Tree where
  arbitrary = do
    maxDepth <- choose (1, 10)
    t <- arbitraryTree maxDepth
    return t

So how do you create an arbitrary MTree? The following code makes it all work.

import Test.QuickCheck
import Control.Monad

data MTree t = And (MTree t) (MTree t)
  | Or (MTree t) (MTree t)
  | Leaf t
  deriving Show

arbitraryMTree :: Arbitrary t => Int -> Gen (MTree t)
arbitraryMTree 0        =
  do
    c <- arbitrary
    return (Leaf c)
arbitraryMTree maxDepth =
  do
    tr <- oneof [arbitraryMTree 0,
                liftM2 And (arbitraryMTree depth') (arbitraryMTree depth'),
                liftM2 Or (arbitraryMTree depth') (arbitraryMTree depth')]
    return tr
  where
    depth' = maxDepth -1

instance Arbitrary t => Arbitrary (MTree t) where
  arbitrary = do
    maxDepth <- choose (1, 10)
    tr <- arbitraryMTree maxDepth
    return tr

This is remarkably neat. QuickCheck, for me, is the killer feature of functional programming!

Returning to the Squad example from the other day. We can create Arbitrary instances of Squad.

import Data.Set
import Data.List
import Test.QuickCheck
import Control.Monad

data Squad a = Squad { players :: (Set a), team :: a}

instance (Ord a, Arbitrary a) => Arbitrary (Squad a) where
  arbitrary =
    do
    ps <- listOf1 arbitrary
    t <- elements ps
    return (Squad (fromList ps) t)

But now I have a complication (and this is where the Squad metaphor breaks down, but please bare with me). Suppose I want a Squad of players where I can in some instances choose a subset of them to play, and in other instances only choose one of them to play. So the Squad still constains a set of players, but the team can sometimes be a subset of the players, and sometimes is a single player. We’ve got to allow instances of TypeSynonyms:

{-# LANGUAGE TypeSynonymInstances #-}
import Data.Set
import Data.List
import Test.QuickCheck
import Control.Monad

data Squad a b = Squad { players :: (Set a), team :: b}
type SetSquad = Squad Char (Set Char)
type SingleSquad = Squad Char Char

instance Arbitrary SetSquad where
  arbitrary =
    do
    ps <- listOf1 arbitrary
    t <- elements (subsequences ps)
    return (Squad (fromList ps) (fromList t))

instance Arbitrary SingleSquad where
  arbitrary =
    do
    ps <- listOf1 arbitrary
    t <- elements ps
    return (Squad (fromList ps) t)

Which is awesome.

Mad props to my homeboy Eric (I’ve been told that’s how one addresses a USian) for listening to my overtired ramblings last night, and for pointing me at numerous ways to implement what I wanted to.

Leave a Reply