-
Notifications
You must be signed in to change notification settings - Fork 30
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
Moved functions from Random to Gen #238
Conversation
There is a test failure because of the implementation of |
There are many tests failing. I analyze just one of them here. From this code to this code is not behavior preserving. |
@TysonMN good catch, I used a local |
Thank you, @adam-becker. Thank you, also, @TysonMN, for reviewing this in #238 (comment) as I haven't looked into the changes yet. Question: Shall we get to this after merging the recheck feature in? Then, the plan is to make a new release, and then review this. |
I also have a question. For this code base are we supposed to use |
@moodmosaic @TysonMN The branch for this PR has been rebased, it should be ready for review again. |
Now that 0.9.0 is released, it is time to resume the review of this PR? |
Yes. And perhaps we can pack this and #247 into the same (major, v10.x) release. |
Rebased and ready for review again. |
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 just did a first round of review (I've only scanned the changes) and left a few nitpicks. Another round of review (looking at the actual impl of the moved functions) should be warranted. We should be good to go then. I've also run Script.fsx, all the stuff in there worked as usual. |
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.
Looks great. I left a comment around filter (the diff there isn't very clear, note that I'm reviewing this from the airport right now as I'm about to board on a plane).
src/Hedgehog/Gen.fs
Outdated
let g' = resize (2 * k + n) g | ||
bind g' <| fun x -> | ||
if p x then | ||
constant (Some x) |
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.
It looks like the actual filtering part was removed. (?)
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.
The filter part is still there, it's this part I believe (267-270):
if p x then
constant (Some x)
else
tryN (k + 1) (n - 1)
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 agree with @moodmosaic. Most of the changes in these lines are from renames or from merging modules. But where did the call to Tree.filter
go?
If not calling Tree.filter
is a bug, then we should make a test for it.
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 agree with @TysonMN, it'd be nice with a test.
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.
@moodmosaic @TysonMN I see what happened here. There were a few tryFilter*
functions, tryFilterRandom
and tryFilter
. When Random
was removed, the tryFilterRandom
function was also removed. The code is close enough that the diff shows them as the same function that changed, when this isn't the case.
I believe we still need the functionality that tryFilterRandom
exposed? Not sure what we want to name the function though.
src/Hedgehog/Gen.fs
Outdated
|> Tree.filter (atLeast (Range.lowerBound size range)) | ||
}) | ||
bind (integral range) <| fun n -> | ||
replicate n g |
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.
Will this implementation behave the same as the old/current one?
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'm not sure if it behaves the same, but I think it will behave as I would expect 😅 Basically just generate a number from the range, and generate that many elements as a list. As long as replicate
has that behavior, and doesn't just duplicate the same element n
times.
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.
@moodmosaic I will add tests for this just to be sure, but I'm pretty confident as is.
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 agree with @adam-becker. It is hard to know if the implementations are exactly the same, but the new implementation is "the right" one (because it doesn't do a deep dive into implementation details).
However, as I pointed out in other comments, a stack overflow bug was introduced in replicate
.
src/Hedgehog/Gen.fs
Outdated
let replicate (times : int) (g : Gen<'a>) : Gen<'a list> = | ||
let rec loop n xs = | ||
if n <= 0 then | ||
constant xs | ||
else | ||
bind g (fun x -> loop (n - 1) (x :: xs)) | ||
loop times [] |
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.
...while this recursion lacks tail calls. The stack will overflow if times
is sufficiently large as in this code.
let n = 10000
Gen.constant 1
|> Gen.replicate n
|> Gen.sample 0 1
|> ignore
This function nearly implements ListGen.sequence
from PR #260. Implementing this function in terms of that one would only be slightly inefficient because of the additional call to List.rev
. Even so, I suggest we first (correctly!...see below) implement traverse
and sequence
, and then implement this replicate
in terms of sequence
.
Furthermore, I now realize that traverse
in PR #260 also lack tail calls. (I wrongly assumed that return!
"magically" avoided this problem.)
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.
The tricky bit with making this tail recursive is that you're multiple functors deep. List<Gen<'a>>
where Gen<'a>
returns a Tree<'a>
for all of the shrinks. The Haskell version implements this with the replicateM
function, but I think Haskell's laziness makes this work. Aside from that, this will be tricky and require some thought.
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.
Probably the simplest thing to do is keep the implementation like it was before. We can keep this refactor in mind and come back to it later.
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.
Actually, I am not sure it is possible to completely avoid stack overflows.
Recall that the type 'a -> 'b
is (covariant) functor in 'b
. The map
function for this functor is function composition (which is either >>
or <<
with only the order of inputs chanding). Our Random<'c>
type is a (covariant) functor in 'c
because it is just an wrapper around the type 'a -> 'b -> 'c
, which is also a (covarant) functor in 'c
. We can simplify things by uncurrying to get back to a function of the form 'a -> 'b
.
I don't know a way to implement map
for the (covariant) functor 'a -> 'b
that won't overflow the stack. It might be impossible.
Specifically, the following test fails.
[<Fact>]
let ``Does function composition overflow the stack? Answer: Yes`` () =
let n = 100000
let f =
id
|> List.replicate n
|> List.fold (>>) id
f ()
With this in mind, I think we should be more willing to allow implementations that can overflow the stack like the one this comment thread is about. Changing the code to put more pressure on the stack will cause it to overflow sooner, but I currently think it is impossible to completely avoid in this case.
In conclusion, I suggest that we no longer hold up this PR because of this recursion that lacks tail calls. Perhaps we can put a comment in the code to remind us of that lack of tails calls.
src/Hedgehog/Random.fs
Outdated
let replicate (times : int) (r : Random<'a>) : Random<List<'a>> = | ||
Random <| fun seed0 size -> | ||
let rec loop seed k acc = | ||
if k <= 0 then | ||
acc | ||
else | ||
let seed1, seed2 = Seed.split seed | ||
let x = unsafeRun seed1 size r | ||
loop seed2 (k - 1) (x :: acc) | ||
loop seed0 times [] |
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.
This recursion uses tail calls, while...
@TysonMN happy to merge this if you think it's in a state that can be merged. I haven't followed after #238 (comment) because I'm still (partially) out and about. |
Whoa! I meant to say that I am NOT completely convinced that removing My current feeling is that we should not do this. I think the separation between My current thinking is that I haven't focused directly on the separation between |
Can you expand on why? I see the little bits you mentioned later, but these seems more like an instinct rather than a firm judgment.
This is why I think removing this module/type will make things simpler going forward.
Not sure how much more they could be separated, they are distinct types/modules now.
I think |
The best example is probably In my study of functional programming, I found found a common way to implement module CD =
let bind (f: 'a -> C<D<'b>>) : C<D<'a>> -> C<D<'b>> =
f
|> DC.traverse
|> C.bind
>> C.map D.flatten
I think that is because of duplicate code like in the above example. In only
It will be easier to fix bugs specific to
|
This PR makes more functions look like |
Oh, by fsharp-hedgehog/src/Hedgehog/Tree.fs Lines 34 to 35 in 00e45b7
|
I've redone this branch entirely, EDIT: Also, lots of functions are now tail-recursive. Including: /cc @moodmosaic @TysonMN |
I am still not convinced that this is a good idea. I will review the current branch and see if I come to a different conclusion. |
Closing this for now. We can re-evaluate using the new contributing process. |
Breaking change, but alluded to in #177
I wasn't able to get the exact type mentioned in that issue, and settled for the type found in this request.
Feel free to close if this is too big of a change.