6

I'm looking at the comments in shrink:

It is tempting to write the last line as [Branch x' l' r' | x' <- shrink x, l' <- shrink l, r' <- shrink r] but this is the wrong thing! It will force QuickCheck to shrink x, l and r in tandem, and shrinking will stop once one of the three is fully shrunk.

I'm a bit puzzled about this, I thought that the problem with that shrink was that we're taking the cartesian product of the shrinks of the subterms, which might be quite large. I don't get why shrinking will stop once one of the three is fully shrunk.

So in other words, I thought the definition above would compute all combinations of shrinks of x, l, and r, but I wouldn't expect shrink to stop once one the the terms is fully shrunk.

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
Damian Nadales
  • 4,907
  • 1
  • 21
  • 34
  • Yeah agree, maybe it's a typo and they meant `[Branch x' l' r' | x' <- shrink x | l' <- shrink l | r' <- shrink r]` back when parallel list comprehensions were in vogue? – luqui Sep 24 '19 at 06:53

1 Answers1

3

The proposed code is wrong, because we only consider shrinks that shrink all three of the terms by at least one step. To see the problem, imagine all three candidates are Int:

x = 1
l = 0
r = 2

Then we have

shrink x = [0]
shrink l = []
shrink r = [1, 0]

A nested list comprehension over these three lists will yield no values, because no suitable candidate for l can be found. So we never try some valid shrinks like (0, 0, 1). The shrink instance for tuples does the right thing (suggesting each shrink where at least one term can be shrunk), so mapping over that solves the problem.

amalloy
  • 89,153
  • 8
  • 140
  • 205
  • 1
    I see, so maybe the comment should be rephrased to say that shrinking *could* stop if one of the three is fully shrink (and not *once*), or something like that... – Damian Nadales Sep 24 '19 at 07:26
  • I mean, even if it's not the case that one is fully shrunk, we skip over some valid shrinks. Imagine (1, 1, 1): there are 7 valid shrinks, but a nested list comprehension over the three shrinks misses stuff like (1, 0, 1), yielding only (0, 0, 0). – amalloy Sep 24 '19 at 07:37
  • 1
    @DamianNadales 1. No, shrinking _will definitely_ stop if one of the components is fully shrunk, as it says. Because then you have something like `[... | x' <- [], ...]` and this is always empty. 2. "and not once" Here "once" means "as soon as", not "one time". – Alexey Romanov Sep 24 '19 at 07:54
  • 1
    Also see the [`successors`](http://hackage.haskell.org/package/successors) package, which gives you an functor to model that “one step in one field” idea. – Joachim Breitner Sep 24 '19 at 08:34