3

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?

Vagif Abilov
  • 9,835
  • 8
  • 55
  • 100
  • Instead of using `System.Random`, can't you use `Gen.choose`? – Mark Seemann Mar 31 '16 at 13:51
  • @MarkSeemann, you can use `choose` instead of `Random` by instantly sampling it, but that would defeat the purpose, because this `choose` won't be part of the resulting generator, but will work as a sort of utility function. No better than `Random`, really. – Fyodor Soikin Mar 31 '16 at 14:12
  • @FyodorSoikin I disagree - you never want to use `System.Random` in generators because it will destroy reproducibility (i.e. same seed returns different results each time) and make shrinking impossible (or at least non-deterministic). – Kurt Schelfthout Apr 01 '16 at 07:08
  • @KurtSchelfthout: Mark suggested to use `Gen.choose` _instead of Random_. If you plug it in place of Random in the OP's first code block, it would be useless, would it not? – Fyodor Soikin Apr 01 '16 at 12:36
  • @FyodorSoikin Just read your comment again - yes, if you use `Gen.choose |> Gen.sample` it's no better than Random. But don't think that's what @MarkSeemann meant :) Confusion all round...but I think we are all saying the same thing. – Kurt Schelfthout Apr 01 '16 at 17:30

2 Answers2

3

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)
  }
Fyodor Soikin
  • 78,590
  • 9
  • 125
  • 172
  • Thanks a lot for an excellent explanation, Fyodor! However I didn't understand what's wrong with Gen.choose that Mark used in his answer. You said it woudn't be a part of the resulting generator, but to me it looks like it is (see Mark's code with gen computational expression). – Vagif Abilov Apr 01 '16 at 05:13
  • Mark misunderstood my comment. He suggested to not just use `Gen.choose`, but use it _instead of Random_. If you replace the use of Random in your first code block with `Gen.choose`, you'd need to sample it right away, which would make it useless. But if you restructure the generator so that `Gen.choose` is properly bound, then it is, of course, not useless. – Fyodor Soikin Apr 01 '16 at 12:33
  • I am sorry I also misunderstood your comment. I thought you were not in favor of Gen.choose at all, I and I found it quite convincing, so I accepted Mark's answer. Now I got the whole picture and of course you came first with the great proposition. – Vagif Abilov Apr 01 '16 at 12:40
2

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 ;)

Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • 1
    Thank your so much! BTW your Pluralsight course was the turning point for me to finally start using FsCheck in my projects, and this led me to my question, so it feels right that you also answered it :-) Your code looks like exactly what I was after (I overlooked gen computational expression which is essential in the solution). I am puzzled why Fyodor disliked using Gen.choose, I need to understand it better. – Vagif Abilov Apr 01 '16 at 05:16
  • And when it comes to non-empty strings, isn't it just to replace Gen.listOf with Gen.nonEmptyListOf? – Vagif Abilov Apr 01 '16 at 05:46
  • @VagifAbilov yes you can use Gen.nonEmptyListOf too, if you just want non-empty strings. Note that the length can then also increase to a number greater than 10. – Kurt Schelfthout Apr 01 '16 at 07:11
  • Oh yes, then the size can be greater than 10. – Vagif Abilov Apr 01 '16 at 07:13
  • @MarkSeeman: It totally baffles me how you failed to notice that your code is almost verbatim what I gave in my answer 13 hour earlier than yours. Bravo! – Fyodor Soikin Apr 01 '16 at 12:27
  • @FyodorSoikin I was offline when I puzzled my suggestion together. After I posted, I did notice, so I upvoted your answer. – Mark Seemann Apr 01 '16 at 13:05