[QuickCheck] an idea for improving 'shrink'

Petr Pudlák petr.mvd at gmail.com
Sun Jul 6 19:13:37 BST 2014


Hi,

I was learning about 'shrink' lately and I was trying to create some
instances. It felt quite awkward until I realized that the operation for
producing shrunk tuples is an applicative functor. I was playing with the
idea for a while and I'm sending an experimental  patch against QuickCheck
master that shows the basics of the idea and how it can help constructing
'shrink' instances. If you feel that this is a good idea, let me know, I'll
work on a full patch.

  Best regards,
  Petr
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://projects.haskell.org/pipermail/quickcheck/attachments/20140706/e7397499/attachment.htm>
-------------- next part --------------
diff --git a/Test/QuickCheck/Arbitrary.hs b/Test/QuickCheck/Arbitrary.hs
index e924696..d8b4101 100644
--- a/Test/QuickCheck/Arbitrary.hs
+++ b/Test/QuickCheck/Arbitrary.hs
@@ -88,6 +88,17 @@ import Data.List
   , nub
   )
 
+import Data.Traversable
+  ( traverse
+  )
+
+import Control.Applicative
+  ( Applicative(..)
+  , (<$>)
+  , liftA2
+  , liftA3
+  )
+
 import Control.Monad
   ( liftM
   , liftM2
@@ -105,6 +116,27 @@ import Data.Typeable
 #endif
 
 --------------------------------------------------------------------------
+-- ** class Shrink
+
+-- | Captures a value together with its shrunk variants.
+data Shrink a = Shrink { sOriginal :: a
+                       , fromShrink :: [a]
+                       }
+  deriving (Eq, Ord, Show)
+
+instance Functor Shrink where
+    fmap f (Shrink x xs) = Shrink (f x) (map f xs)
+
+instance Applicative Shrink where
+    pure x = Shrink x []
+    (Shrink f fs) <*> (Shrink x xs) =
+        Shrink (f x) (map ($ x) fs ++ map f xs)
+
+-- | Adds more shrunk values to the current ones.
+(>.) :: [a] -> Shrink a -> Shrink a
+ys >. Shrink x xs = Shrink x (ys ++ xs)
+infixr 2 >.
+
 -- ** class Arbitrary
 
 -- | Random generation and shrinking of values.
@@ -179,7 +211,28 @@ class Arbitrary a where
   -- after deriving @Generic@ and @Typeable@ for your type. However, if your data type has any
   -- special invariants, you will need to check that 'genericShrink' can't break those invariants.
   shrink :: a -> [a]
-  shrink _ = []
+  shrink = fromShrink . shrinkA
+
+  -- ...
+  --
+  -- For example, suppose we have the following implementation of binary trees:
+  --
+  -- > data Tree a = Nil | Branch a (Tree a) (Tree a)
+  --
+  -- We can then define 'shrinkA' as follows:
+  --
+  -- > shrink (Branch x l r) =
+  -- >   -- shrink Branch to Nil
+  -- >   [Nil] ++
+  -- >   -- shrink to subterms
+  -- >   [l, r] >.
+  -- >   -- recursively shrink subterms
+  -- >   Branch <$> shrinkA x <*> shrinkA l <*> shrinkA r
+  -- > shrink x = pure x
+  --
+  -- ...
+  shrinkA :: a -> Shrink a
+  shrinkA = pure
 
 #ifndef NO_GENERICS
 -- | Shrink a term to any of its immediate subterms,
@@ -263,33 +316,29 @@ instance Arbitrary Ordering where
 instance Arbitrary a => Arbitrary (Maybe a) where
   arbitrary = frequency [(1, return Nothing), (3, liftM Just arbitrary)]
 
-  shrink (Just x) = Nothing : [ Just x' | x' <- shrink x ]
-  shrink _        = []
+  shrinkA (Just x) = [Nothing] >. Just <$> shrinkA x
+  shrinkA v        = pure v
 
 instance (Arbitrary a, Arbitrary b) => Arbitrary (Either a b) where
   arbitrary = oneof [liftM Left arbitrary, liftM Right arbitrary]
 
-  shrink (Left x)  = [ Left  x' | x' <- shrink x ]
-  shrink (Right y) = [ Right y' | y' <- shrink y ]
+  shrinkA (Left x)  = Left  <$> shrinkA x
+  shrinkA (Right y) = Right <$> shrinkA y
 
 instance Arbitrary a => Arbitrary [a] where
   arbitrary = sized $ \n ->
     do k <- choose (0,n)
        sequence [ arbitrary | _ <- [1..k] ]
 
-  shrink xs = shrinkList shrink xs
+  shrinkA xs = shrinkList shrinkA xs
 
 -- | Shrink a list of values given a shrinking function for individual values.
-shrinkList :: (a -> [a]) -> [a] -> [[a]]
+shrinkList :: (a -> Shrink a) -> [a] -> Shrink [a]
 shrinkList shr xs = concat [ removes k n xs | k <- takeWhile (>0) (iterate (`div`2) n) ]
-                 ++ shrinkOne xs
+                 >. traverse shr xs
  where
   n = length xs
 
-  shrinkOne []     = []
-  shrinkOne (x:xs) = [ x':xs | x'  <- shr x ]
-                  ++ [ x:xs' | xs' <- shrinkOne xs ]
-
   removes k n xs
     | k > n     = []
     | null xs2  = [[]]
@@ -312,8 +361,7 @@ instance (Integral a, Arbitrary a) => Arbitrary (Ratio a) where
 
 instance (RealFloat a, Arbitrary a) => Arbitrary (Complex a) where
   arbitrary = liftM2 (:+) arbitrary arbitrary
-  shrink (x :+ y) = [ x' :+ y | x' <- shrink x ] ++
-                    [ x :+ y' | y' <- shrink y ]
+  shrinkA (x :+ y) = liftA2 (:+) (shrinkA x) (shrinkA y)
 
 #ifndef NO_FIXED
 instance HasResolution a => Arbitrary (Fixed a) where
@@ -326,18 +374,14 @@ instance (Arbitrary a, Arbitrary b)
  where
   arbitrary = liftM2 (,) arbitrary arbitrary
 
-  shrink (x, y) =
-       [ (x', y) | x' <- shrink x ]
-    ++ [ (x, y') | y' <- shrink y ]
+  shrinkA (x, y) = liftA2 (,) (shrinkA x) (shrinkA y)
 
 instance (Arbitrary a, Arbitrary b, Arbitrary c)
       => Arbitrary (a,b,c)
  where
   arbitrary = liftM3 (,,) arbitrary arbitrary arbitrary
 
-  shrink (x, y, z) =
-    [ (x', y', z')
-    | (x', (y', z')) <- shrink (x, (y, z)) ]
+  shrinkA (x, y, z) = liftA3 (,,) (shrinkA x) (shrinkA y) (shrinkA z)
 
 instance (Arbitrary a, Arbitrary b, Arbitrary c, Arbitrary d)
       => Arbitrary (a,b,c,d)


More information about the QuickCheck mailing list