Consider a Discriminated Union:
type DU = | Foo of string | Bar of int | Baz of decimal * float | Qux of bool
I'd like to create a list of DU
values with FsCheck, but I want none of the values to be of the Qux
case.
This predicate already exists:
let isQux = function Qux _ -> true | _ -> false
First attempt
My first attempt to create a list of DU
values without the Qux
case was something like this:
type DoesNotWork =
static member DU () = Arb.from<DU> |> Arb.filter (not << isQux)
[<Property(MaxTest = 10 , Arbitrary = [| typeof<DoesNotWork> |])>]
let repro (dus : DU list) =
printfn "%-5b : %O" (dus |> List.exists isQux |> not) dus
Running this seems to produce a stack overflow, so I assume that what happens behind the scene is that Arb.from<DU>
calls DoesNotWork.DU
.
Second attempt
Then I tried this:
type DoesNotWorkEither =
static member DU () =
Arb.generate<DU>
|> Gen.suchThat (not << isQux)
|> Arb.fromGen
[<Property(MaxTest = 10 , Arbitrary = [| typeof<DoesNotWorkEither> |])>]
let repro (dus : DU list) =
printfn "%-5b : %O" (dus |> List.exists isQux |> not) dus
Same problem as above.
Verbose solution
This is the best solution I've been able to come up with so far:
type WithoutQux =
static member DU () =
[
Arb.generate<string> |> Gen.map Foo
Arb.generate<int> |> Gen.map Bar
Arb.generate<decimal * float> |> Gen.map Baz
]
|> Gen.oneof
|> Arb.fromGen
[<Property(MaxTest = 10 , Arbitrary = [| typeof<WithoutQux> |])>]
let repro (dus : DU list) =
printfn "%-5b : %O" (dus |> List.exists isQux |> not) dus
This works, but has the following disadvantages:
- It seems like a lot of work
- It doesn't use the already available
isQux
function, so it seems to subtly violate DRY - It doesn't really filter, but rather only produces the desired cases (so filters only by omission).
- It isn't particularly maintainable, because if I ever add a fifth case to
DU
, I would have to remember to also add aGen
for that case.
Is there a more elegant way to tell FsCheck to filter out Qux
values?
Instead of
Arb.generate
, which tries to use the registered instance for the type, which is the instance you're trying to define, which causes an infinite loop, useArb.Default.Derive()
which will go straight to the reflective based generator.https://github.com/fscheck/FsCheck/blob/master/src/FsCheck/Arbitrary.fs#L788-788
This is such a common mistake we should be able to solve out of the box in FsCheck: https://github.com/fscheck/FsCheck/issues/109
The particular problem in the OP can be solved like this: