Skip to content

Commit d66336c

Browse files
authored
Added All and Any monoids (#397)
* Added All and Any monoids `All` is a monoid build around `.&&.`. It is useful when writing complex properties which check multiple conditions. Since it is a monoid it allows one to use `foldMap` which is often much more ergonomic than using `conjoin`. `All` satisfies `monoid` laws up to `isSuccess`, unless one is using `checkCoverage` & `cover`. I'd argue this is not a problem since `checkCoverage` and `cover` are most often added at the top of the property. This patch also adds `Any` monoid build around `.||.`. Tests are also included.
1 parent f43aead commit d66336c

5 files changed

Lines changed: 248 additions & 1 deletion

File tree

QuickCheck.cabal

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,17 @@ library
9292
else
9393
Build-depends: splitmix >= 0.1.0.2 && <0.2
9494

95+
if impl(hugs)
96+
cpp-options: -DNO_SEMIGROUP -DNO_EXISTENTIAL_FIELD_SELECTORS
97+
9598
-- Modules that are always built.
9699
Exposed-Modules:
97100
Test.QuickCheck,
98101
Test.QuickCheck.Arbitrary,
99102
Test.QuickCheck.Gen,
100103
Test.QuickCheck.Gen.Unsafe,
101104
Test.QuickCheck.Monadic,
105+
Test.QuickCheck.Monoids,
102106
Test.QuickCheck.Modifiers,
103107
Test.QuickCheck.Property,
104108
Test.QuickCheck.Test,
@@ -127,7 +131,7 @@ library
127131
cpp-options: -DNO_TEMPLATE_HASKELL
128132

129133
if !impl(ghc >= 8.0)
130-
cpp-options: -DNO_CALLSTACK
134+
cpp-options: -DNO_CALLSTACK -DNO_SEMIGROUP
131135

132136
if !impl(ghc >= 7.4)
133137
cpp-options: -DNO_CTYPES_CONSTRUCTORS -DNO_FOREIGN_C_USECONDS
@@ -272,3 +276,14 @@ Test-Suite test-quickcheck-discard
272276
hs-source-dirs: tests
273277
main-is: DiscardRatio.hs
274278
build-depends: base, QuickCheck
279+
280+
Test-Suite test-quickcheck-monoids
281+
type: exitcode-stdio-1.0
282+
Default-language: Haskell2010
283+
hs-source-dirs: tests
284+
main-is: Monoids.hs
285+
build-depends: base, QuickCheck
286+
if !impl(ghc >= 8.4)
287+
cpp-options: -DNO_SEMIGROUP_SUPERCLASS
288+
if !impl(ghc >= 8.0)
289+
cpp-options: -DNO_SEMIGROUP_CLASS

make-hugs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ find "$TOPDIR/src" -name '*.hs' | while read -r src; do
1919
-DNO_SAFE_HASKELL -DNO_POLYKINDS -DNO_MONADFAIL -DNO_TIMEOUT \
2020
-DNO_NEWTYPE_DERIVING -DNO_TYPEABLE -DNO_GADTS -DNO_TRANSFORMERS \
2121
-DNO_DEEPSEQ -DNO_EXTRA_METHODS_IN_APPLICATIVE -DNO_CALLSTACK \
22+
-DNO_SEMIGROUP -DNO_EXISTENTIAL_FIELD_SELECTORS \
2223
"$src" > "$tgt"
2324
done
2425

src/Test/QuickCheck.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,8 +315,10 @@ module Test.QuickCheck
315315
, (.&.)
316316
, (.&&.)
317317
, conjoin
318+
, Every (..)
318319
, (.||.)
319320
, disjoin
321+
, Some (..)
320322
-- ** What to do on failure
321323
#ifndef NO_TYPEABLE
322324
, Witness(..)
@@ -355,6 +357,7 @@ module Test.QuickCheck
355357
import Test.QuickCheck.Gen
356358
import Test.QuickCheck.Arbitrary
357359
import Test.QuickCheck.Modifiers
360+
import Test.QuickCheck.Monoids
358361
import Test.QuickCheck.Property hiding ( Result(..) )
359362
import Test.QuickCheck.Test
360363
import Test.QuickCheck.Exception

src/Test/QuickCheck/Monoids.hs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
{-# LANGUAGE CPP #-}
2+
{-# LANGUAGE ExistentialQuantification #-}
3+
4+
module Test.QuickCheck.Monoids
5+
( Every (..)
6+
, Some (..)
7+
) where
8+
9+
#ifndef NO_SEMIGROUP
10+
import Data.List.NonEmpty as NonEmpty
11+
import Data.Semigroup (Semigroup (..))
12+
#else
13+
import Data.Monoid (Monoid (..))
14+
#endif
15+
import Test.QuickCheck.Property
16+
17+
-- | Conjunction monoid built with `.&&.`.
18+
--
19+
-- Use `property @Every` as an accessor which doesn't leak
20+
-- existential variables.
21+
--
22+
-- Note: monoid laws are satisfied up to `isSuccess` unless one is using
23+
-- `checkCoverage`.
24+
--
25+
#ifndef NO_EXISTENTIAL_FIELD_SELECTORS
26+
data Every = forall p. Testable p => Every { getEvery :: p }
27+
#else
28+
data Every = forall p. Testable p => Every p
29+
#endif
30+
31+
instance Testable Every where
32+
property (Every p) = property p
33+
34+
#ifndef NO_SEMIGROUP
35+
instance Semigroup Every where
36+
Every p <> Every p' = Every (p .&&. p')
37+
sconcat = Every . conjoin . NonEmpty.toList
38+
39+
instance Monoid Every where
40+
mempty = Every True
41+
mappend = (<>)
42+
mconcat = Every . conjoin
43+
#else
44+
instance Monoid Every where
45+
mempty = Every True
46+
mappend (Every p) (Every p') = Every (p .&&. p')
47+
mconcat = Every . conjoin
48+
#endif
49+
50+
51+
-- | Disjunction monoid built with `.||.`.
52+
--
53+
-- Use `property @Some` as an accessor which doesn't leak
54+
-- existential variables.
55+
--
56+
-- Note: monoid laws are satisfied up to `isSuccess` unless one is using
57+
-- `checkCoverage`.
58+
--
59+
#ifndef NO_EXISTENTIAL_FIELD_SELECTORS
60+
data Some = forall p. Testable p => Some { getSome :: p }
61+
#else
62+
data Some = forall p. Testable p => Some p
63+
#endif
64+
65+
instance Testable Some where
66+
property (Some p) = property p
67+
68+
#ifndef NO_SEMIGROUP
69+
instance Semigroup Some where
70+
Some p <> Some p' = Some (p .||. p')
71+
sconcat = Some . disjoin . NonEmpty.toList
72+
73+
instance Monoid Some where
74+
mempty = Some False
75+
mappend = (<>)
76+
mconcat = Some . disjoin
77+
#else
78+
instance Monoid Some where
79+
mempty = Some False
80+
mappend (Some p) (Some p') = Some (p .||. p')
81+
mconcat = Some . disjoin
82+
#endif

tests/Monoids.hs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
{-# LANGUAGE CPP #-}
2+
{-# LANGUAGE TemplateHaskell #-}
3+
{-# LANGUAGE ConstraintKinds #-}
4+
5+
#if __GLASGOW_HASKELL__ >= 800
6+
{-# OPTIONS_GHC -Wno-orphans #-}
7+
#endif
8+
{-# LANGUAGE FlexibleInstances #-}
9+
10+
module Main (main) where
11+
12+
#ifndef NO_SEMIGROUP_CLASS
13+
import Data.List.NonEmpty
14+
import Data.Semigroup (Semigroup (..))
15+
#else
16+
import Data.Monoid (Monoid (..), (<>))
17+
#endif
18+
19+
import Test.QuickCheck
20+
21+
#ifdef NO_SEMIGROUP_CLASS
22+
type Semigroup = Monoid
23+
sconcat :: Monoid a => [a] -> a
24+
sconcat = mconcat
25+
#endif
26+
27+
instance Arbitrary Every where
28+
arbitrary = oneof [ pure $ Every True
29+
, pure $ Every False
30+
, pure $ Every (counterexample "False" False)
31+
, pure $ Every (counterexample "True" True)
32+
, pure $ Every (ioProperty (return True))
33+
, pure $ Every (ioProperty (return False))
34+
, pure $ Every (checkCoverage $ cover 100 True "" True)
35+
, pure $ Every (checkCoverage $ cover 100 True "" False)
36+
, pure $ Every (checkCoverage $ cover 100 False "" False)
37+
]
38+
39+
40+
instance Arbitrary Some where
41+
arbitrary = oneof [ pure $ Some True
42+
, pure $ Some False
43+
, pure $ Some (counterexample "False" False)
44+
, pure $ Some (counterexample "True" True)
45+
, pure $ Some (ioProperty (return True))
46+
, pure $ Some (ioProperty (return False))
47+
, pure $ Some (checkCoverage $ cover 100 True "" True)
48+
, pure $ Some (checkCoverage $ cover 100 True "" False)
49+
, pure $ Some (checkCoverage $ cover 100 False "" True)
50+
, pure $ Some (checkCoverage $ cover 100 False "" False)
51+
]
52+
53+
54+
newtype Fail a = Fail a
55+
56+
instance Arbitrary (Fail Every) where
57+
arbitrary = oneof [ Fail <$> (arbitrary :: Gen Every)
58+
, pure $ Fail $ Every (checkCoverage $ cover 100 False "" True)
59+
]
60+
61+
62+
check_associative_law :: (Testable p, Semigroup p) => Blind p -> Blind p -> Blind p -> Property
63+
check_associative_law (Blind a) (Blind b) (Blind c) = ioProperty $ do
64+
x <- isSuccess <$> quickCheckWithResult args (a <> (b <> c))
65+
y <- isSuccess <$> quickCheckWithResult args ((a <> b) <> c)
66+
return (x == y)
67+
68+
69+
#ifndef NO_SEMIGROUP_SUPERCLASS
70+
check_unit_law :: (Testable p, Monoid p) => Blind p -> Property
71+
#else
72+
check_unit_law :: (Testable p, Monoid p, Semigroup p) => Blind p -> Property
73+
#endif
74+
check_unit_law (Blind a) = ioProperty $ do
75+
x <- isSuccess <$> quickCheckWithResult args (a <> mempty)
76+
y <- isSuccess <$> quickCheckWithResult args (mempty <> a)
77+
z <- isSuccess <$> quickCheckWithResult args a
78+
return (x == y .&&. y == z)
79+
80+
81+
#ifndef NO_SEMIGROUP_CLASS
82+
check_sconcat_law :: (Testable p, Semigroup p) => Blind p -> Blind p -> Property
83+
check_sconcat_law (Blind a) (Blind b) = ioProperty $ do
84+
x <- isSuccess <$> quickCheckWithResult args (sconcat $ a :| [b])
85+
y <- isSuccess <$> quickCheckWithResult args (a <> b)
86+
return (x == y)
87+
#endif
88+
89+
90+
#ifndef NO_SEMIGROUP_SUPERCLASS
91+
check_mconcat_law :: (Testable p, Monoid p) => Blind p -> Blind p -> Property
92+
#else
93+
check_mconcat_law :: (Testable p, Monoid p, Semigroup p) => Blind p -> Blind p -> Property
94+
#endif
95+
check_mconcat_law (Blind a) (Blind b) = ioProperty $ do
96+
x <- isSuccess <$> quickCheckWithResult args (mconcat [a, b])
97+
y <- isSuccess <$> quickCheckWithResult args (a <> b)
98+
return (x == y)
99+
100+
101+
--
102+
-- Auxiliary definitions
103+
--
104+
105+
args :: Args
106+
args = stdArgs { chatty = False, maxShrinks = 0 }
107+
108+
--
109+
-- Properties
110+
--
111+
112+
prop_every_associative :: Blind Every -> Blind Every -> Blind Every -> Property
113+
prop_every_associative = check_associative_law
114+
115+
prop_every_unit :: Blind Every -> Property
116+
prop_every_unit = check_unit_law
117+
118+
prop_every_unit_fail :: Blind (Fail Every) -> Property
119+
prop_every_unit_fail (Blind (Fail a)) =
120+
expectFailure $ check_unit_law (Blind a)
121+
122+
#ifndef NO_SEMIGROUP_CLASS
123+
prop_every_sconcat_law :: Blind Every -> Blind Every -> Property
124+
prop_every_sconcat_law = check_sconcat_law
125+
#endif
126+
127+
prop_every_mconcat_law :: Blind Every -> Blind Every -> Property
128+
prop_every_mconcat_law = check_mconcat_law
129+
130+
prop_some_associative :: Blind Some -> Blind Some -> Blind Some -> Property
131+
prop_some_associative = check_associative_law
132+
133+
prop_some_unit :: Blind Some -> Property
134+
prop_some_unit = check_unit_law
135+
136+
#ifndef NO_SEMIGROUP_CLASS
137+
prop_some_sconcat_law :: Blind Some -> Blind Some -> Property
138+
prop_some_sconcat_law = check_sconcat_law
139+
#endif
140+
141+
prop_some_mconcat_law :: Blind Some -> Blind Some -> Property
142+
prop_some_mconcat_law = check_mconcat_law
143+
144+
return []
145+
main = $quickCheckAll
146+

0 commit comments

Comments
 (0)