-
Notifications
You must be signed in to change notification settings - Fork 158
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
Implement property expression à la Hedgehog #404
Comments
Computation expressions can only be created for monads, because you need a Unfortunately, |
I'd be very happy to have this work just for generators, without shrinking. |
You can use the |
Yes, and it's also a pain (or at the very least messy) if you need values from several generators. |
I don't think there is any technical roadblock. Prop.forAll is essentially bind, and Property is essentially [<Property>]
let ``constrain returns original value if already inside interval`` () =
let sortedTuples = Arb.generate<int*int*int> |> Gen.map G.sortTuple3 |> Arb.fromGen
Prop.forAll sortedTuples (fun (x1, x, x2) -> test <@ constrain (x1, x2) x = x @>) |
Thanks for the tip, that looks better and would certainly scale a bit better with regards to the number of generated values needed (though not that much as far as I can see, since you'd need to I'm still more a fan of generating values using |
You just get other "boilerplate" in return like Also you can do [<Property>]
let ``constrain returns original value if already inside interval`` (t3) =
let (x1, x, x2) = G.sortTuple3 t3
test <@ constrain (x1, x2) x = x @> With shrinking. We'll need more substantial examples than this. |
Ooh, nice! I totally missed that. For this example, at least. :) I'll see if I can come up with some real examples that are more revealing. If I can't, it may all just be in my head - you never know with people these days. |
A cute idea I just had to share: let (|SortedTuple|) = G.sortTuple3
[<Property>]
let ``constrain returns original value if already inside interval`` (SortedTuple (x1,x,x2)) =
test <@ constrain (x1, x2) x = x @> |
Just a quick sanity check here: Consider the following code: let increasing3<'a when 'a : comparison> =
Arb.from<'a*'a*'a> |> Arb.filter (fun (x1, x2, x3) -> x1 < x2 && x2 < x3)
[<Property>]
let ``returns upper value if above interval`` () =
increasing3<int> |> Prop.forAll <| fun (x1, x2, x) ->
test <@ constrain (x1, x2) x = x2 @> I can see exactly two ways of achieving this:
Am I right in that there are no other ways to do this? |
And one other question for the same code snippet: Although it might not be needed in this particular case, in order to decrease the chance of throwing away results I considered sorting the tuple (as previously mentioned) before Edit: I discovered let sort3 (x1, x2, x3) =
[x1; x2; x3] |> List.sort |> (fun [x1;x2;x3] -> x1, x2, x3)
let isSorted3 (x1, x2, x3) =
x1 <= x2 && x2 <= x3
let isStrictlyIncreasing3 (x1, x2, x3) =
x1 < x2 && x2 < x3
let increasing3<'a when 'a : comparison> =
Arb.from<'a*'a*'a>
|> Arb.mapFilter sort3 isSorted3 // for efficiency
|> Arb.filter isStrictlyIncreasing3 I guess I could also have used Another edit: Now I'm having trouble reproducing the example that led me to this conclusion. Maybe I haven't understood it after all. |
Okay, here's an example: Combining arbitraries. Say I want a test that needs two distinct integers and a string. I have already defined the following: let distinct2<'a when 'a : equality> =
Arb.from<'a*'a> |> Arb.filter (fun (x1, x2) -> x1 <> x2)
let alphaNumStr =
let alphaNum = ['A'..'Z'] @ ['a'..'z'] @ ['0'..'9']
Arb.from<string>
|> Arb.filter (fun s -> s |> Seq.forall (fun c -> alphaNum |> List.contains c)) Now, how do I combine these? I can't find any way of doing that (there might be an easy way I don't know - nothing would be better). The following obviously doesn't work, but serves to illustrate what I want: [<Property>]
let ``what is supposed to happen should happen`` () =
Prop.forAll (distinct2<int>, alphaNumStr) <| fun (x1, x2) s
... These arbitraries are fairly simple, but one could easily imagine the need for combining more complex ones. With computation expressions, this is trivial, since each arbitrary (or generator) would be at the right side of a separate |
You could use [<Property>]
let ``returns upper value if above interval`` (x1,x2,x) =
x1 < x2 && x2 < x ==> lazy( test <@ constrain (x1, x2) x = x2 @> ) |
Yes, that's exactly what |
[<Property>]
let ``what is supposed to happen should happen`` () =
Prop.forAll distinct2<int> (fun x1 ->
Prop.forAll alphaNumStr (fun x2 ->
s
)) This also shows that |
Oh. Well then. Didn't think of that 🙃 Thanks! |
But defining nested functions (or in general, nested constructs) like that, isn't that exactly what computation expressions are for? I'm open to this being subjective, but I would say that for any such case where you need to use multiple arbitraries, it's definitely cleaner to say [<Property>]
let ``what is supposed to happen should happen`` () = property {
let! x1, x2 = distinct2<int>
let! s = alphaNumStr
... } instead of [<Property>]
let ``what is supposed to happen should happen`` () =
Prop.forAll distinct2<int> <| fun (x1, x2) ->
Prop.forAll alphaNumStr <| fun s ->
... I would also say it's easier to newcomers. I'm giving an F# course at my workplace today, and I would find it much easier to explain the first one, which lets you use normal F# syntax as long as you wrap it in Note by the way that I haven't included any other boilerplate than |
They're syntactic sugar for nested functions, when they are needed for monadic binds. I feel that What isn't a matter of taste is that using computation expressions carries performance penalties that are non-obvious - extra Delay and Run calls and closures are generated. Bind, or |
I didn't know about the performance impacts. Out of interest, do you have any ballpark estimate of how large they'd be in this context? I also agree that In any case, feel free to close this if you don't want to implement it. :-) |
Yes I don't think it's worth the work in v2. If #403 works out in v3, I expect you get this as well. |
One thing that pains me in FsCheck as compared to Hedgehog is generating values. In Hedgehog, for a suitably defined
G.int
(generates ints without any more fuss) andG.sortTuple3
(sorts a 3-tuple) I can do this:In FsCheck, for a suitably defined
G.sortTuple3
, this looks much worse, even without a shrinker:I have a few hundred other tests that also point in the same direction: Using a computation expression for properties that allows you to generate values directly inside the property looks much clearer. Could such a computation expression be implemented in FsCheck?
Or perhaps I have missed some other nicer way to accomplish this in FsCheck?
The text was updated successfully, but these errors were encountered: