How to easily filter out a discriminated union case in FsCheck?

899 views Asked by At

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 a Gen for that case.

Is there a more elegant way to tell FsCheck to filter out Qux values?

2

There are 2 answers

0
Kurt Schelfthout On BEST ANSWER

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, use Arb.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:

type WithoutQux =
    static member DU () = Arb.Default.Derive () |> Arb.filter (not << isQux)

[<Property(MaxTest = 10 , Arbitrary = [| typeof<WithoutQux> |])>]
let repro (dus : DU list) =
    printfn "%-5b : %O" (dus |> List.exists isQux |> not) dus
2
theimowski On

The below should work:

type DU = | Foo of string | Bar of int | Baz of decimal * float | Qux of bool
let isQux = function Qux _ -> true | _ -> false

let g = Arb.generate<DU> |> Gen.suchThat (not << isQux) |> Gen.listOf

type DoesWork =
    static member DU () = Arb.fromGen g

[<Property(MaxTest = 10 , Arbitrary = [| typeof<DoesWork> |])>]
let repro (dus : DU list) =
    printfn "%-5b : %O" (dus |> List.exists isQux |> not) dus

Note I used Gen.listOf at the end - seems like FsCheck fails to generate itself a list with the given generator