Skip to content
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

Closed
wants to merge 17 commits into from
202 changes: 109 additions & 93 deletions src/Hedgehog/Gen.fs
Original file line number Diff line number Diff line change
@@ -1,53 +1,52 @@
namespace Hedgehog

open System
open Hedgehog.Numeric

/// A generator for values and shrink trees of type 'a.
[<Struct>]
type Gen<'a> =
| Gen of Random<Tree<'a>>
| Gen of (Seed -> Size -> Tree<'a>)

module Gen =

let ofRandom (r : Random<Tree<'a>>) : Gen<'a> =
Gen r
let private unsafeRun (seed : Seed) (size : Size) (Gen(r) : Gen<'a>) : Tree<'a> =
r seed size

let toRandom (Gen r : Gen<'a>) : Random<Tree<'a>> =
r
let run (seed : Seed) (size : Size) (Gen(r) : Gen<'a>) : Tree<'a> =
r seed (max 1 size)

let delay (f : unit -> Gen<'a>) : Gen<'a> =
Random.delay (toRandom << f) |> ofRandom
Gen (fun seed size -> unsafeRun seed size (f ()))

let tryFinally (after : unit -> unit) (m : Gen<'a>) : Gen<'a> =
toRandom m |> Random.tryFinally after |> ofRandom
Gen (fun seed size ->
try
unsafeRun seed size m
finally
after ())

let tryWith (k : exn -> Gen<'a>) (m : Gen<'a>) : Gen<'a> =
toRandom m |> Random.tryWith (toRandom << k) |> ofRandom

let create (shrink : 'a -> seq<'a>) (random : Random<'a>) : Gen<'a> =
random |> Random.map (Tree.unfold id shrink) |> ofRandom
Gen (fun seed size ->
try
unsafeRun seed size m
with
x -> unsafeRun seed size (k x))

let constant (x : 'a) : Gen<'a> =
Tree.singleton x |> Random.constant |> ofRandom

let private bindRandom (k : 'a -> Random<Tree<'b>>) (m : Random<Tree<'a>>) : Random<Tree<'b>> =
Hedgehog.Random (fun seed0 size ->
let seed1, seed2 =
Seed.split seed0

let run (seed : Seed) (random : Random<'x>) : 'x =
Random.run seed size random

Tree.bind (k >> run seed2) (run seed1 m))
Gen (fun _ _ -> Tree.singleton x)

let bind (k : 'a -> Gen<'b>) (m : Gen<'a>) : Gen<'b> =
toRandom m |> bindRandom (toRandom << k) |> ofRandom
Gen (fun seed size ->
let (seed1, seed2) = Seed.split seed

let mapRandom (f : Random<Tree<'a>> -> Random<Tree<'b>>) (g : Gen<'a>) : Gen<'b> =
toRandom g |> f |> ofRandom
Tree.bind (k >> run seed2 size) (run seed1 size m))

let mapTree (f : Tree<'a> -> Tree<'b>) (g : Gen<'a>) : Gen<'b> =
mapRandom (Random.map f) g
Gen (fun seed size ->
g
|> unsafeRun seed size
|> f)

let map (f : 'a -> 'b) (g : Gen<'a>) : Gen<'b> =
mapTree (Tree.map f) g
Expand Down Expand Up @@ -128,6 +127,7 @@ module Gen =
let noShrink (g : Gen<'a>) : Gen<'a> =
let drop (Node (x, _)) =
Node (x, Seq.empty)

mapTree drop g

/// Apply an additional shrinker to all generated trees.
Expand All @@ -144,12 +144,14 @@ module Gen =

/// Used to construct generators that depend on the size parameter.
let sized (f : Size -> Gen<'a>) : Gen<'a> =
Random.sized (toRandom << f) |> ofRandom
Gen (fun seed size ->
unsafeRun seed size (f size))

/// Overrides the size parameter. Returns a generator which uses the
/// given size instead of the runtime-size parameter.
let resize (n : int) (g : Gen<'a>) : Gen<'a> =
mapRandom (Random.resize n) g
Gen (fun seed _ ->
unsafeRun seed n g)

/// Adjust the size parameter, by transforming it with the given
/// function.
Expand All @@ -164,10 +166,12 @@ module Gen =
/// Generates a random number in the given inclusive range.
let inline integral (range : Range<'a>) : Gen<'a> =
// https://github.com/hedgehogqa/fsharp-hedgehog/pull/239
range
|> Random.integral
|> Random.map (range |> Range.origin |> Shrink.createTree)
|> ofRandom
Gen (fun seed size ->
let (lower, upper) = Range.bounds size range
let (value, _) = Seed.nextBigInt (toBigInt lower) (toBigInt upper) seed

fromBigInt value
|> Shrink.createTree (Range.origin range))

//
// Combinators - Choice
Expand Down Expand Up @@ -209,32 +213,31 @@ module Gen =
let f n =
let smallWeights =
xs
|> List.map fst
|> List.scan (+) 0
|> List.pairwise
|> List.takeWhile (fun (a, _) -> a < n)
|> List.map snd
|> List.toArray
|> Seq.map fst
|> Seq.scan (+) 0
|> Seq.pairwise
|> Seq.takeWhile (fun (a, _) -> a < n)
|> Seq.map snd
|> Seq.toArray
let length = smallWeights |> Array.length
Shrink.createTree 0 (length - 1)
|> Tree.map (fun i -> smallWeights.[i])
|> Tree.map (Array.get smallWeights)

gen {
let! n =
Range.constant 1 total
|> integral
|> toRandom
|> Random.map (Tree.outcome >> f)
|> ofRandom
return! pick n xs
}
let g = Gen (fun seed size ->
Range.constant 1 total
|> integral
|> unsafeRun seed size
|> Tree.outcome
|> f)

g |> bind (fun n -> pick n xs)

/// Randomly selects one of the gens in the list.
/// <i>The input list must be non-empty.</i>
let choice (xs0 : seq<Gen<'a>>) : Gen<'a> = gen {
let xs = Array.ofSeq xs0
if Array.isEmpty xs then
return crashEmpty "xs" xs
return crashEmpty "xs"
else
let! ix = Range.ofArray xs |> integral
return! Array.item ix xs
Expand Down Expand Up @@ -265,41 +268,37 @@ module Gen =

/// More or less the same logic as suchThatMaybe from QuickCheck, except
/// modified to ensure that the shrinks also obey the predicate.
let private tryFilterRandom (p : 'a -> bool) (r0 : Random<Tree<'a>>) : Random<Option<Tree<'a>>> =
let rec tryN k = function
| 0 ->
Random.constant None
| n ->
let r = Random.resize (2 * k + n) r0
r |> Random.bind (fun x ->
if p (Tree.outcome x) then
Tree.filter p x |> Some |> Random.constant
else
tryN (k + 1) (n - 1))

Random.sized (tryN 0 << max 1)
let private tryFilterRandom (predicate : 'a -> bool) (gen : Gen<'a>) seed size : Option<Tree<'a>> =
let rec loop countUp countDown seed size =
if countDown = 0 then
None
else
let seed1, seed2 = Seed.split seed
let tree = run seed1 (2 * countUp + countDown) gen

if predicate (Tree.outcome tree) then
Some (Tree.filter predicate tree)
else
loop (countUp + 1) (countDown - 1) seed2 size

loop 0 (max 1 size) seed size

/// Generates a value that satisfies a predicate.
let filter (p : 'a -> bool) (g : Gen<'a>) : Gen<'a> =
let rec loop () =
toRandom g
|> tryFilterRandom p
|> Random.bind (function
| None ->
Random.sized (fun n ->
Random.resize (n + 1) (Random.delay loop))
| Some x ->
Random.constant x)

loop ()
|> ofRandom
let rec loop seed size =
let seed1, seed2 = Seed.split seed
match tryFilterRandom p g seed1 size with
| None -> loop seed2 (size + 1)
| Some x -> x

Gen(loop)

/// Tries to generate a value that satisfies a predicate.
let tryFilter (p : 'a -> bool) (g : Gen<'a>) : Gen<'a option> =
toRandom g
|> tryFilterRandom p
|> Random.bind (OptionTree.sequence >> Random.constant)
|> ofRandom
Gen (fun seed size ->
let seed1, _ = Seed.split seed
let optionTree = tryFilterRandom p g seed1 size
OptionTree.sequence optionTree)

/// Runs an option generator until it produces a 'Some'.
let some (g : Gen<'a option>) : Gen<'a> =
Expand All @@ -326,13 +325,23 @@ module Gen =

/// Generates a list using a 'Range' to determine the length.
let list (range : Range<int>) (g : Gen<'a>) : Gen<List<'a>> =
Random.sized (fun size -> random {
let! k = Random.integral range
let! xs = Random.replicate k (toRandom g)
return Shrink.sequenceList xs
|> Tree.filter (atLeast (Range.lowerBound size range))
})
|> ofRandom
Gen (fun seed size ->
let (seed1, seed2) = Seed.split seed
let (lower, upper) = Range.bounds size range
let (value, _) = Seed.nextBigInt (toBigInt lower) (toBigInt upper) seed1

let rec loop seed countDown acc =
if countDown <= 0 then
acc
|> Shrink.sequenceList
|> Tree.filter (atLeast (Range.lowerBound size range))
else
let seed1, seed2 = Seed.split seed
let value = unsafeRun seed1 size g
loop seed2 (countDown - 1) (value :: acc)

let (seed1, _) = Seed.split seed2
loop seed1 (fromBigInt value) [])

/// Generates an array using a 'Range' to determine the length.
let array (range : Range<int>) (g : Gen<'a>) : Gen<array<'a>> =
Expand Down Expand Up @@ -446,8 +455,11 @@ module Gen =

/// Generates a random 64-bit floating point number.
let double (range : Range<double>) : Gen<double> =
Random.double range
|> create (Shrink.towardsDouble (Range.origin range))
Gen (fun seed size ->
let (lower, upper) = Range.bounds size range
let (value, _) = Seed.nextDouble lower upper seed
let shrink = Shrink.towardsDouble (Range.origin range)
Tree.unfold id shrink value)

/// Generates a random 64-bit floating point number.
let float (range : Range<float>) : Gen<float> =
Expand Down Expand Up @@ -505,10 +517,15 @@ module Gen =
//

let sampleTree (size : Size) (count : int) (g : Gen<'a>) : List<Tree<'a>> =
let seed = Seed.random ()
toRandom g
|> Random.replicate count
|> Random.run seed size
let rec loop seed k acc =
if k <= 0 then
acc
else
let seed1, seed2 = Seed.split seed
let x = unsafeRun seed1 size g
loop seed2 (k - 1) (x :: acc)

loop (Seed.random ()) count []

let sample (size : Size) (count : int) (g : Gen<'a>) : List<'a> =
sampleTree size count g
Expand All @@ -518,8 +535,7 @@ module Gen =
/// if you want another size then you should explicitly use 'resize'.
let generateTree (g : Gen<'a>) : Tree<'a> =
let seed = Seed.random ()
toRandom g
|> Random.run seed 30
run seed 30 g

let renderSample (gen : Gen<'a>) : string =
String.concat Environment.NewLine [
Expand Down
1 change: 0 additions & 1 deletion src/Hedgehog/Hedgehog.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ https://github.com/hedgehogqa/fsharp-hedgehog/blob/master/doc/index.md
<Compile Include="Tree.fs" />
<Compile Include="OptionTree.fs" />
<Compile Include="Range.fs" />
<Compile Include="Random.fs" />
<Compile Include="Shrink.fs" />
<Compile Include="Gen.fs" />
<Compile Include="ListGen.fs" />
Expand Down
22 changes: 0 additions & 22 deletions src/Hedgehog/Linq/Gen.fs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,9 @@ type Gen private () =
static member FromValue (value : 'T) : Gen<'T> =
Gen.constant value

static member FromRandom (random : Random<Tree<'T>>) : Gen<'T> =
Gen.ofRandom random

static member Delay (func : Func<Gen<'T>>) : Gen<'T> =
Gen.delay func.Invoke

static member Create (shrink : Func<'T, seq<'T>>, random : Random<'T>) : Gen<'T> =
Gen.create shrink.Invoke random

static member Sized (scaler : Func<Size, Gen<'T>>) : Gen<'T> =
Gen.sized scaler.Invoke

Expand Down Expand Up @@ -211,14 +205,6 @@ type GenExtensions private () =
return projection.Invoke (a, b)
}

[<Extension>]
static member SelectRandom (gen : Gen<'T>, binder : Func<Random<Tree<'T>>, Random<Tree<'TResult>>>) : Gen<'TResult> =
Gen.mapRandom binder.Invoke gen

[<Extension>]
static member SelectTree (gen : Gen<'T>, binder : Func<Tree<'T>, Tree<'TResult>>) : Gen<'TResult> =
Gen.mapTree binder.Invoke gen

[<Extension>]
static member Select (gen : Gen<'T>, mapper : Func<'T, 'TResult>) : Gen<'TResult> =
Gen.map mapper.Invoke gen
Expand Down Expand Up @@ -260,14 +246,6 @@ type GenExtensions private () =
static member String (gen : Gen<char>, range : Range<int>) : Gen<string> =
Gen.string range gen

[<Extension>]
static member ToGen (random : Random<Tree<'T>>) : Gen<'T> =
Gen.ofRandom random

[<Extension>]
static member ToRandom (gen : Gen<'T>) : Random<Tree<'T>> =
Gen.toRandom gen

[<Extension>]
static member TryFinally (gen : Gen<'T>, after : Action) : Gen<'T> =
Gen.tryFinally after.Invoke gen
Expand Down
4 changes: 2 additions & 2 deletions src/Hedgehog/Property.fs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ module Property =
loop 0<shrinks>

let private reportWith' (args : PropertyArgs) (config : PropertyConfig) (p : Property<unit>) : Report =
let random = toGen p |> Gen.toRandom
let gen = toGen p

let nextSize size =
if size >= 100 then
Expand All @@ -156,7 +156,7 @@ module Property =
Status = GaveUp }
else
let seed1, seed2 = Seed.split args.Seed
let result = Random.run seed1 args.Size random
let result = Gen.run seed1 args.Size gen
let nextArgs = {
args with
Seed = seed2
Expand Down
Loading