Vagif Abilov
Vagif Abilov

Reputation: 9991

How to recursively use FsCheck generators?

I use FsCheck for property-based testing, so I defined a set a generators for custom types. Some of types are composed of others, and there are generators for all of them. Having defined a generator for Alphanumeric type, I want to define a generator for RelativeUrl type, and RelativeUrl is list of 1-9 Alphanumeric values separated by slash symbol. Here's the definition that works (Alpanumeric has "Value" property that converts it to String):

static member RelativeUrl() =
    Gen.listOfLength (System.Random().Next(1, 10)) <| Generators.Alphanumeric()
    |> Gen.map (fun list -> String.Join("/", list |> List.map (fun x -> x.Value)) |> RelativeUrl)

Even though it's quite simple I don't like that I use Random.Next method instead of using FsCheck random generators. So I tried to redefine it like this:

static member RelativeUrl_1() =
    Arb.generate<byte> 
    |> Gen.map int 
    |> Gen.suchThat (fun x -> x > 0 && x <= 10)
    |> Gen.map (fun length -> Gen.listOfLength length <| Generators.Alphanumeric())
    |> Gen.map (fun list -> String.Join("/", list))

Compiler accepts it but in fact it's wrong: a "list" in the last statement is not a list of Alphanumeric values but a Gen. Next attempt:

static member RelativeUrl() =
    Arb.generate<byte> 
    |> Gen.map int 
    |> Gen.suchThat (fun x -> x > 0 && x <= 10)
    |> Gen.map (fun length -> Gen.listOfLength length <| Generators.Alphanumeric())
    |> Gen.map (fun list -> list |> Gen.map (fun elem -> String.Join("/", elem |> List.map (fun x -> x.Value))  |> RelativeUrl))

But this doesn't work either: I am getting back Gen of Gen of RelativeUrl, not Gen of RelativeUrl. So what would be a proper way of combining generators at different levels?

Upvotes: 3

Views: 404

Answers (2)

Fyodor Soikin
Fyodor Soikin

Reputation: 80744

Gen.map has the signature (f: 'a -> 'b) -> Gen<'a> -> Gen<'b> - that is, it takes a function from 'a to 'b, then a Gen<'a>, and returns a Gen<'b>. One might think of it as "applying" the given function to what's "inside" of the given generator.

But the function you're providing in your map call is, in fact, int -> Gen<Alphanumeric list> - that is, it returns not some 'b, but more specifically Gen<'b>, so the result of the whole expression becomes Gen<Gen<Alphanumeric list>>. This is why Gen<Alphanumeric list> shows up as the input in the next map. All by design.

The operation you really want is usually called bind. Such function would have a signature (f: 'a -> Gen<'b>) -> Gen<'a> -> Gen<'b>. That is, it would take a function that produces another Gen, not a naked value.

Unfortunately, for some reason, Gen doesn't expose bind as such. It is available as part of the gen computation expression builder or as operator >>= (which is de facto standard operator for representing bind).

Given the above explanation, you can rephrase your definition like this:

static member RelativeUrl_1() =
    Arb.generate<int> 
    |> Gen.suchThat (fun x -> x > 0 && x <= 10)
    >>= (fun length -> Gen.listOfLength length <| Generators.Alphanumeric())
    |> Gen.map (fun list -> String.Join("/", list))

You may also consider using a computation expression to build you generator. Unfortunately, there is no where defined for the gen expression builder, so you still have to use suchThat to filter. But fortunately, there is a special function Gen.choose for producing a value in a given range:

static member RelativeUrl_1() =
  gen {
    // let! length = Arb.generate<int> |> Gen.suchThat (fun l -> l > 0 && l <= 10)
    let! length = Gen.choose (1, 10)
    let! list = Gen.listOfLength length <| Generators.Alphanumeric()
    return String.Join ("/", list)
  }

Upvotes: 3

Mark Seemann
Mark Seemann

Reputation: 233150

The comment from Fyodor Soikin suggests that Gen.choose isn't useful, so perhaps I'm missing something, but here's my attempt:

open System
open FsCheck

let alphanumericChar = ['a'..'z'] @ ['A'..'Z'] @ ['0'..'9'] |> Gen.elements
let alphanumericString =
    alphanumericChar |> Gen.listOf |> Gen.map (List.toArray >> String)

let relativeUrl = gen {
    let! size = Gen.choose (1, 10)
    let! segments = Gen.listOfLength size alphanumericString
    return String.concat "/" segments }

This seems to work:

> Gen.sample 10 10 relativeUrl;;
val it : string list =
  ["IC/5p///G/H/ur/vs//"; "l/mGe8spXh//au2WgdL/XvPJhey60X";
   "dxr/0y/1//P93/Ca/D/"; "R/SMJ3BvsM/Fzw4oifN71z"; "52A/63nVPM/TQoICz";
   "Co/1zTNKiCwt1/y6fwDc7U1m/CSN74CwQNl/olneBaJEB/RFqKiCa41l//ADo2MIUPFM/vG";
   "Zm"; "AxRpJ/fP/IOvpX/3yo"; "0/6QuDwiEgC/IpXRO8GA/E7UB8"; "jK/C/X/E4/AL3"]

Notice that my definition of alphanumericString may generate empty strings, so sometimes, as you can see from the above FSI sample output, it'll generate relative URL values with empty segments.

I'll leave it as an exercise to the reader to define non-empty alphanumeric strings. If you need help with this, please ask another question and ping me ;)

Upvotes: 2

Related Questions