Commits

Aleksey Khudyakov  committed dd8f07e

Make properties more composable

  • Participants
  • Parent commits a3f4e8a

Comments (0)

Files changed (1)

File Test/QuickCheck/Property/Common.hs

 {-# LANGUAGE FlexibleContexts  #-}
 {-# LANGUAGE FlexibleInstances #-}
 -- | 
+-- There're two most import
+--
 -- /Fixing type/
 --
--- All properties in this library are polymorphic. Because of it
--- following expression will be rejected by compiler (assuming that
--- @someFunction@ is polymorphic.
+-- All properties in this library are polymorphic. For example
+-- property for checking associativity of 'mappend' could have
+-- following type:
 --
--- > quickCheck (prop_Associative someFunction)
+-- > prop_mappend :: (Eq a, Monoid a) => a -> a -> a -> Bool
 --
--- Method for fixing type is nessesary.
+-- But if one tries to pass this expression to 'quickCheck' GHC will
+-- rightfully complain that type is too generic. Indeed there is no
+-- way to figure out what is type of a. Obvious way to fix type of @a@
+-- is to add type signature. However it's too cumbersome to write
+-- signature for 3 parameter function. 
 --
--- /Comparig for equality/
+-- Another approach was taken instead. All properties take dummy
+-- parameter which fix type:
+--
+-- > prop_Mappend :: (Eq a, Monoid a) => T a -> a -> a -> a -> Bool
+--
+-- 'T' is phanom typed unit. It ensures that only type information
+-- could be passed to function. For example test invokation could look
+-- like this:
+--
+-- > quickCheck $ prop_Mappend (T :: T [Int])
+--
+-- By convention all user supplied parameters are placed before T and
+-- all quickcheck supplied parameters are after T.
+--
+-- /Comparing for equality/
 --
 -- A lot of QuickCheck properties have form @expression = another
 -- expression@. It's natural to compare them for equality however not
--- all types have 'Eq' instance. For example functions and hence many
+-- all types have 'Eq' instance. Functions for example and hence many
 -- monads.
 --
 -- There are three generic ways to compare values for equality.
 --  3. Most generic: use custom comparison function.
 --
 -- Functions 'eq', 'eqOn' and 'eqWith' provide this functionality.
+--
+-- Additionally properties of same type could be composed using
+-- boolean expressions. 
 module Test.QuickCheck.Property.Common (
     -- * Comparison for equality
     Equal(..)
   , appEqual
   , Equalable(..)
+    -- ** Compose properties
+  , (.==.)
+  , (.&&.)
+  , (.||.)
+  , notE
+    -- ** Convert to QuickCheck properies
   , eq
   , eqOn
   , eqWith
 
 -- | Values to be compared for equality
 data Equal a = Equal a a
+             | NotE (Equal a)
+             | AndE (Equal a) (Equal a)
+             | OrE  (Equal a) (Equal a)
 
--- | Perform comparison using custom function
 appEqual :: (a -> a -> Bool) -> Equal a -> Bool
 appEqual f (Equal a b) = f a b
+appEqual f (NotE e)    = not $ appEqual f e
+appEqual f (AndE e g)  = appEqual f e && appEqual f g
+appEqual f (OrE  e g)  = appEqual f e && appEqual f g
 
 -- | Recurse through function to apply comparison to 'Equal'.
 class Equalable a where
   -- | Type which should be compared for equality
   type Result a :: *
-  -- | Result of comparison
+  -- | Result of comparison. Could be passed to 'quickCheck'
   type Compared a :: *
-  -- | Compare 
+  -- | Compare value using custom comparison function
   equalWith :: (Result a -> Result a -> Bool) -> a -> Compared a
-                
+  -- | Map property 
+  mapEqual  :: (Equal (Result a) -> Equal (Result a)) -> a -> a
+  -- | Zip properties
+  zipEquals :: (Equal (Result a) -> Equal (Result a) -> Equal (Result a)) -> a -> a -> a
+
+-- | Convenience sinonym for 'Equal'. Delay comparison for equality
+(.==.) :: a -> a -> Equal a
+(.==.) = Equal
+
+-- | Both properties are true.
+(.&&.) :: Equalable a => a -> a -> a
+(.&&.) = zipEquals AndE
+
+-- | One of properties is true
+(.||.) :: Equalable a => a -> a -> a
+(.||.) = zipEquals OrE
+
+-- | Property is false
+notE :: Equalable a => a -> a
+notE = mapEqual NotE
+
 instance Equalable (Equal a) where
   type Result   (Equal a) = a
   type Compared (Equal a) = Bool
   equalWith = appEqual
+  mapEqual  = id
+  zipEquals = id
 
--- FIXME: Equalable is not composable. No way to compose two
---        properties into one
-instance Equalable [Equal a] where
-  type Result   [Equal a] = a
-  type Compared [Equal a] = Bool
-  equalWith = all . appEqual
-  
 instance Equalable a => Equalable (x -> a) where
   type Result   (x -> a) = Result a
   type Compared (x -> a) = x -> Compared a
   equalWith f fun = equalWith f . fun
-
+  mapEqual  f fun = mapEqual  f . fun
+  zipEquals f fun1 fun2 = \x -> zipEquals f (fun1 x) (fun2 x)
 
 -- | Compare values using @==@ 
 eq :: (Equalable a, Eq (Result a)) => a -> Compared a