-
Notifications
You must be signed in to change notification settings - Fork 407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove shrinking from gen-based Prop.forAll #440
Conversation
Shrinking is only safe when used with Arbitrary. Generators can have a smaller range than the global shrinking, which results in invalid shrunken values. This fixes issues like typelevel#129.
This breaks binary compatibility (that's a great build check to have). I'm not sure what the suggested approach would be. It can actually be made binary compatible if we keep the |
It's probably more acceptable to:
Or maybe they should be deprecated without changing the behaviour? |
@dwijnand Thanks for the comment/suggestions. Keeping the implicits is probably the safe way to go. I might wait for a conceptual thumbs up from a maintainer though, I'm worried that it may not be something they're interested in. I'm not as sure about introducing even more methods though. People can already use Thanks again. |
Sorry, that was a clumsy way of saying that I don’t know whether Rickard or others consider this an issue or not. If people are interested I’m very happy to make whatever changes are required. Thanks again. |
Restore bincompat & deprecate Gen.forAll w/ Shrink
Swap the impls, so the deprecated API calls the not deprecated API.
Makes sense to me!
I got curious so I tried it out and was able restore the binary API, which I also deprecated: charleso#1.
Good point. So I deprecated Gen-taking |
Deprecate Gen-taking forAllNoShrink
Yeah that's brilliant. Thank you. |
Seems I forgot to define the deprecation version in my last change. I'm logging off, but I can fix it up when I'm back (unless you beat me to it). |
Fixed. There were (funnily enough) a few warnings around calling the deprecated |
Another person caught out by the forAll behaviour #443 (comment) |
@charleso @dwijnand Sorry for my unresponsiveness. I think your approach here makes sense. In one way it makes ScalaCheck less powerful since it removes shrinking that could have been useful in many situations. On the other hand it removes a big source of confusion that a lot of people have stumbled on. The question is now if we should release a version with unchanged behavior and deprecated methods before actually making this change. I wonder if we also can remove the hideous |
If you prefer, I'm happy if you'd like to do things that way. 1.14.1 with deprecated, same behaviour methods, 1.15.0 with this PR?
Sounds good. |
/** Universal quantifier for an explicit generator. Shrinks failed arguments | ||
* with the default shrink function for the type */ | ||
/** Universal quantifier for an explicit generator. */ | ||
@deprecated("Use the variant without Shrink. Here only for bincompat.", "1.15.0") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does one still use shrinking? In QuickCheck, the default for forAll
is no shrinking, but there is still support for shrinking with forAllShrink
. From what I see so far, that isn't available here. This reads like turning off shrinking entirely, when the proposal was changing the default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does one still use shrinking?
It's still there, but only for the variants of the forAll
function with the implicit Arbitrary
. I'm proposing to disable it for the forAll
functions that take Gen
.
def forAll[A1,P] (f: A1 => P)(implicit p: P => Prop, a1: Arbitrary[A1], s1: Shrink[A1]): Prop
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In QuickCheck, the default for forAll is no shrinking
Do you mean ScalaCheck? QuickCheck has always only shrunk with Arbitrary (if implemented for a specific instance) and never with Gen, which is what I'm proposing to do in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I missed the nuance of the change then.
So you're proposing four cases:
forAll
and explicitGen
s with shrinking disabledforAll
with implicitArbitrary
s with shrinking preservedforAllShrink
and explicitGen
s with shrinking preservedforAllNoShrink
with implicitArbitrary
s
And still supporting all the possible arities of each.
Right now, ScalaCheck does:
forAll
and explicitGen
s with shrinkingforAll
with implicitArbitrary
s with shrinkingforAllNoShrink
with explicitGen
sforAllNoShrink
with implicitArbitrary
s
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So you're proposing four cases:
Yeah something like that. The forAllShrink
already exists, but takes a manual shrink function. I'm not touching that. I'm only changing the behaviour of forAll
with Gen
which is where the problem is. Everything else stays the same.
I suspect what I've proposed at the moment might have to be tweaked based on @rickynils comment and my own counter-question below (which is totally fine).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW I've been working around this issue for years by using implicit val noShrink: Shrink[MyType] = Shrink.shrinkAny
(couldn't use forAllNoShrink
because of scalatest).
I don't think Gen
with implicit Shrink
makes any sense. But I like the idea of combining them. Then the usability issue could be solved by adding a withShrink(implicit shrink: Shrink[A])
method on Gen
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then the usability issue could be solved by adding a withShrink(implicit shrink: Shrink[A]) method on Gen.
I'm curious. I hope you don't mind me asking. What Gen
s are you constructing/using for your code base, and in particular what is the range of the values? And in what circumstances would you want to summon an implicit
Shrink
?
I ask, because I don't really think the implicit part is a good idea (ie withShrink(shrink: Shrink[A])
would be what I would suggest). But I don't have enough data points to know what people are actually doing with Shrink. I only have my biases. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ask, because I don't really think the implicit part is a good idea (ie withShrink(shrink: Shrink[A]) would be what I would suggest). But I don't have enough data points to know what people are actually doing with Shrink. I only have my biases. :)
I tend to agree here, I don't think I would be using the implicitness here very often, but sometimes it might come in handy (say I have my own type with implicit Shrink[MyType]
) and then I do Gen.listOf(arbitrary[MyType]).withShrink
then shrinking would be automatically propagated to the list generator, but I could still provide explicit shrinkers for other generators.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Explicit shrinks (ie. forAllShrink with arity > 1) would be extremely useful for me.
My workflow for working around this issue has always been to write Gen and Shrink instances in parallel, and to wrap them (case class AsciiString(string: String), Gen[AsciiString], Shrink[AsciiString]) if there is ambiguity (because that's less annoying than passing in the Pretty instances).
@charleso 's case class GenShrink[A](gen: Gen[A], shrink: Shrink[A]) looks perfect for this, especially .filter could pretty easily operate on both sides.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@woparry Thanks for the comment, it's good to hear from someone who is using this.
Just curious, any reason you don't provide an implicit Arbitrary
to make that Gen
as the global/implicit version? Is it just that Gen
works with the global Shrink
, or something else?
I don't particularly have an agenda here, other than to stop people getting confused about the shrinking, I'm super happy to do whatever you think is best for ScalaCheck. I'm just wondering out-loud what upgrading might look like. If we deprecate users upgrading to 1.14.1 will most likely switch every I guess I'm wondering if we shouldn't re-add the non-shrinking non-deprecated Again, I'd like to help get this change through in whatever form makes sense. I'm actually pretty indecisive and so I'm happy if you have an opinion on how you want the change(s) implemented and I can do the leg work. |
I've created a PR that just deprecates the |
An implicit is being deprecated, so the upgrading is the difficult part of this. Deprecating a method typically will result in its subsequent removal, or a subsequent removal of some signature of the method. It's not possible to provide a deprecation time period that gives the old version and simultaneously offers a new alternative without the implicits, since the compiler would be confused about conflicting signatures. And a As a result, when the implicit is removed the user's code will still compile. If they miss or ignore the deprecation warning, there will be no indication of the behavior change. Since most test suites succeed when ScalaCheck is upgraded, there won't be any indication that the shrinking policy changed for It wasn't a poor choice to use an implicit for shrinking in ScalaCheck. This is a major behavior change in the library, so it's unsurprising this change requires delicate handling. The steps I would propose:
|
@charleso First of all, thanks for working on this. I'm sorry it didn't get more attention from me back when you were actively working on it. In the long run, I think ScalaCheck 2 will likely move to combining generation and shrinking, similar to other modern libraries like Hedgehog. That would mean moving away from having a separate Given the new I was thinking of closing out this PR but I wanted to check-in with you first about your thoughts. |
@non Thanks again for the detailed response! I completely agree that a Hedgehog "integrated shrinking" is absolutely 100% the way to go if there's an appetite for it. Even though I'm the author of the Scala Hedgehog library, I strongly believe the wider Scala community would greatly benefit if ScalaCheck gets the same shrinking capability (in the process making Scala Hedgehog mostly redundant).
I guess I can't help (for the last time I promise) being a broken record and say I still believe disabling shrinking for Anyway, I can see I'm not getting any traction with that argument, and I'm not trying to antagonise anyone over it. I'll close the PR now but I'm happy to re-open or help push something through if there's any subsequent interest. :) Thanks again for really stepping up to help maintain ScalaCheck, it's already made a huge difference and I'm excited about its future. Good luck! |
@charleso I think if this was a less high-profile library I would do exactly what you suggest and just start ignoring the Thanks again for your work, and I'm sure we'll be consulting you on the new shrinking design once we get there! ✨ |
Shrinking is only safe when used with Arbitrary. Generators can have a smaller range than the global shrinking, which results in invalid shrunken values.
This fixes issues like #129, #260, #317, #370, #442 and #443.
I suspect this is going to be controversial and I completely understand if it's not accepted. I'm opening this to show how I suggest it should be tackled, but it obviously affects people who are currently relying on this behaviour.
EDIT: Note that Haskell's QuickCheck
forAll
doesn't shrink in this instance either