F# 如何递归使用FsCheck生成器?

F# 如何递归使用FsCheck生成器?,f#,fscheck,F#,Fscheck,我使用FsCheck进行基于属性的测试,因此我为自定义类型定义了一组生成器。其中一些类型由其他类型组成,所有类型都有生成器。在为字母数字类型定义了生成器之后,我想为RelativeUrl类型定义一个生成器,RelativeUrl是由1-9个字母数字值组成的列表,由斜杠符号分隔。以下是有效的定义(Alpanumeric具有将其转换为字符串的“Value”属性): 静态成员RelativeUrl()= Gen.listOfLength(System.Random().Next(1,10))Gen.m

我使用FsCheck进行基于属性的测试,因此我为自定义类型定义了一组生成器。其中一些类型由其他类型组成,所有类型都有生成器。在为字母数字类型定义了生成器之后,我想为RelativeUrl类型定义一个生成器,RelativeUrl是由1-9个字母数字值组成的列表,由斜杠符号分隔。以下是有效的定义(Alpanumeric具有将其转换为字符串的“Value”属性):

静态成员RelativeUrl()=
Gen.listOfLength(System.Random().Next(1,10))Gen.map(趣味列表->字符串.Join(“/”,list |>list.map(趣味x->x.Value))|>RelativeUrl)
虽然很简单,但我不喜欢使用Random.Next方法,而不是使用FsCheck随机生成器。所以我试着这样重新定义它:

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))
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))
static member RelativeUrl_1()=
生成
|>通用地图
|>Gen.suchThat(fun x->x>0&&x Gen.map(fun length->Gen.listOfLength Gen.map(fun list->String.Join(“/”,list))
编译器接受它,但事实上它是错误的:最后一条语句中的“列表”不是字母数字值列表,而是一个Gen。下一次尝试:

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))
静态成员RelativeUrl()=
生成
|>通用地图
|>Gen.suchThat(fun x->x>0&&x Gen.map(fun length->Gen.listOfLength Gen.map(fun list->list |>Gen.map(fun elem->String.Join(“/”,elem |>list.map(fun x->x.Value))|>RelativeUrl))

但这也不起作用:我要找回的是RelativeUrl的Gen,而不是RelativeUrl的Gen。那么,将不同级别的生成器组合在一起的合适方式是什么呢?

Gen.map
有签名
(f:'a->'b)->Gen
-也就是说,它从
'a
'b
,然后是
Gen
。人们可能会认为它将给定的函数“应用”到给定生成器的“内部”

但是您在
map
调用中提供的函数实际上是
int->Gen或as(这实际上是表示
bind
的标准运算符)

根据上述解释,您可以将您的定义改写为:

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

来自Fyodor Soikin的评论表明,
Gen.choose
没有用处,因此我可能遗漏了一些东西,但我的尝试如下:

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 }
这似乎有效:

> 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"]
请注意,我对
alphanumericString
的定义可能会生成空字符串,因此有时,正如您从上面的FSI示例输出中看到的,它会生成带有空段的相对URL值


我将把定义非空字母数字字符串作为练习留给读者。如果您需要这方面的帮助,请问另一个问题并ping我;)

代替使用
系统。随机
,你不能使用
Gen.choose
?@MarkSeemann,你可以使用
choose
代替
Random
,方法是立即对其进行采样,但这将无法达到目的,因为这
choose
将不是结果生成器的一部分,而是作为一种实用函数工作比
Random
更好,真的。@FyodorSoikin我不同意-你永远不想在生成器中使用
System.Random
,因为它会破坏再现性(即同一种子每次返回不同的结果),并使收缩不可能(或至少不确定)@KurtSchelfthout:Mark建议使用
Gen.choose
而不是Random。如果你在OP的第一个代码块中插入Random来代替Random,它将是无用的,不是吗?@FyodorSoikin只需再次阅读你的评论-是的,如果你使用
Gen.choose |>Gen.sample
它并不比Random好。但不要认为这是@MarkSeemann意思是:)到处都是混乱…但我想我们都在说同样的话。非常感谢你的精彩解释,费奥多!但是我不明白Gen出了什么问题。选择他回答中使用的标记。你说过它不会是结果生成器的一部分,但对我来说它看起来像是(参见带有gen计算表达式的Mark代码).Mark误解了我的评论。他建议不要只使用
Gen.choose
,而是使用它,而不要使用Random。如果用
Gen.choose
替换第一个代码块中的Random,则需要立即对其进行采样,这将使其无效。但是如果您重新构造生成器,使
Gen.choose
正确运行当然,这不是毫无用处的。很抱歉,我也误解了你的评论。我认为你根本不赞成Gen.choose,我和我觉得它很有说服力,所以我接受了马克的回答。现在我了解了全部情况,当然你首先提出了这个伟大的建议。非常感谢你!顺便说一句,你的多视课程就是tu我终于开始在我的项目中使用FsCheck了,这让我想到了我的问题,所以你也回答了这个问题,感觉很对:-)你的代码看起来和我想要的完全一样(我忽略了解决方案中必不可少的gen计算表达式)。我不明白为什么Fyodor不喜欢使用Gen.choose,我需要更好地理解它。当涉及到非空字符串时,不是仅仅用Gen.nonEmptyListOf替换Gen.listOf吗?@VagifAbilov是的,如果您只想要非空字符串,您也可以使用Gen.nonEmptyListOf。请注意,长度也可以增加到大于10的数字。哦,y是的,那么大小就可以大于10。@MarkSeeman:你怎么没有注意到你的代码几乎与我在回答中给出的代码一字不差呢?比你的答案早13个小时。太棒了!