Yes, the error is due to pure mempty
, but that doesn't mean pure mempty
is wrong. Let's look there first.
It helps a lot to look at the types involved in the definition mempty = pure mempty
:
mempty :: ZipList a
mempty = (pure :: a -> ZipList a) (mempty :: a)
Basically, we're going to use the pure
operation to create a ZipList
out of the mempty
of type a
. It helps from here to look at the definition of pure
for ZipList
:
pure :: a -> ZipList a
pure x = ZipList (repeat x)
In total, mempty
for ZipList a
is going to be a ZipList
containing the infinitely repeating list of mempty
values of the underlying type a
.
Back to this error you're getting. When you try to run the test monoid
over ZipList (Sum Int)
, QuickCheck is going to test a sequence of properties.
- The first two check the left identity and right identity properties. What these do is generate values of type
x :: ZipList (Sum Int)
and verify that x <> mempty = mempty <> x = x
.
- The third checks that for any two values
x, y :: ZipList (Sum Int)
, we have that x
mappend y = x <> y
.
- The fourth checks that for any list of values
x :: [ZipList (Sum Int)]
, folding these with mappend
is the same as mconcat
ing them.
Before I continue, it's really important to note that when I say "for any value", I really mean that QuickCheck is using the Arbitrary
instance of the said type to generate values of that type. Furthermore, the Arbitrary
instance for ZipList a
is the same as the Arbitrary
instance for [a]
but then wrapped in ZipList
. Lastly, the Arbitrary
instance for [a]
will never produce an infinite list (because those will cause problems when you're checking for equality, like going into an infinite loop or overflowing the stack), so these "for any values" of type ZipList (Sum Int)
will never be infinite either.
Specifically, this means that QuickCheck will never arbitrarily generate the value mempty :: ZipList a
because this is an infinite list.
So why do the first 3 pass but the last one fails with a stack overflow? In the first three tests, we never end up trying to compare an infinite list to an infinite list. Let's see why not.
- In the first two tests, we're looking at
x <> mempty == x
and mempty <> x == x
. In both cases, x
is one of our "arbitrary" values, which will never be infinite, so this equality will never go into an infinite loop.
- In the third test, we're generating two finite ZipLists
x
and y
and mappend
ing them together. Nothing about this will be infinite.
- In the third case, we're generating a list of ZipLists and
mconcat
enating the list. But, what happens if the list is empty? Well, mconcat [] = mempty
, and folding an empty list produces mempty
. This means, if the empty list is generated as the arbitrary input (which is perfectly possible), then the test will try to confirm that an infinite list is equal to another infinite list, which will always result in a stack overflow or black hole.
How can you fix this? I can come up with two methods:
You can define your own version of EqProp
for ZipList
so that it only compares equality on some finite prefix of the list. This would likely involve making a newtype wrapper (perhaps newtype MonZipList a = MonZipList (ZipList a)
), deriving a bunch of instances, and then writing an EqProp
one by hand. This will probably work but is a little inelegant.
You can write your own version of monoid
that uses a different version of the fourth test. For instance, if you restrict it so that the test only uses non-empty lists, then you won't have any problem. To do this, you should start by looking at the definition of the monoid
property tests. Notice that it currently defines the "mconcat" property as property mconcatP
where
mconcatP :: [a] -> Property
mconcatP as = mconcat as =-= foldr mappend mempty as
Using QuickCheck's own NonEmptyList
class, you can rewrite this for your purposes as:
mconcatP :: NonEmptyList a -> Property
mconcatP (NonEmptyList as) = mconcat as =-= foldr mappend mempty as
Obviously, this is a slightly weaker condition, but at least it's one that won't hang.