Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

f#

fscheck

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?

like image 503
Mark Seemann Avatar asked Jun 26 '15 08:06

Mark Seemann


People also ask

When to use Discriminated Union?

Discriminated unions are useful for heterogeneous data; data that can have special cases, including valid and error cases; data that varies in type from one instance to another; and as an alternative for small object hierarchies. In addition, recursive discriminated unions are used to represent tree data structures.

What are Discriminated Union?

A discriminated union is a union data structure that holds various objects, with one of the objects identified directly by a discriminant. The discriminant is the first item to be serialized or deserialized. A discriminated union includes both a discriminant and a component.

How do you declare a discriminated union shape?

For example, consider the following declaration of a Shape type. The preceding code declares a discriminated union Shape, which can have values of any of three cases: Rectangle, Circle, and Prism. Each case has a different set of fields. The Rectangle case has two named fields, both of type float, that have the names width and length.

How to use case identifiers without qualifying the name of the Union?

Normally, the case identifiers can be used without qualifying them with the name of the union. If you want the name to always be qualified with the name of the union, you can apply the RequireQualifiedAccess attribute to the union type definition. In F# Discriminated Unions are often used in domain-modeling for wrapping a single type.

What are discriminated unions?

Discriminated unions are similar to union types in other languages, but there are differences. As with a union type in C++ or a variant type in Visual Basic, the data stored in the value is not fixed; it can be one of several distinct options. Unlike unions in these other languages, however, each of the possible options is given a case identifier.

What is accessibility for discriminated unions?

Accessibility for discriminated unions defaults to public. For example, consider the following declaration of a Shape type. The preceding code declares a discriminated union Shape, which can have values of any of three cases: Rectangle, Circle, and Prism. Each case has a different set of fields.


2 Answers

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

like image 190
theimowski Avatar answered Oct 20 '22 23:10

theimowski


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
like image 42
Kurt Schelfthout Avatar answered Oct 21 '22 01:10

Kurt Schelfthout