我使用FsCheck进行基于属性的测试,因此为自定义类型定义了一个生成器集。一些类型由其他类型组成,并且所有类型都有生成器。为字母数字类型定义了一个生成器之后,我想为RelativeUrl类型定义一个生成器,并且RelativeUrl是由1-9个字母数字值(由斜杠符号分隔)的列表。这是有效的定义(字母数字具有“Value”属性,可将其转换为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)
即使很简单,我也不喜欢使用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))
编译器接受它,但实际上它是错误的:最后一条语句中的“列表”不是字母数字值的列表,而是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一代,而不是RelativeUrl的Gen一代。那么将不同级别的发电机组合在一起的正确方法是什么?
最佳答案
Gen.map
具有签名(f: 'a -> 'b) -> Gen<'a> -> Gen<'b>
-即,它需要从'a
到'b
的函数,然后是Gen<'a>
,并返回Gen<'b>
。可能会认为它是将给定函数“应用”到给定生成器的“内部”。
但是实际上,您在map
调用中提供的函数是int -> Gen<Alphanumeric list>
-也就是说,它不返回某些'b
,而是更具体地返回Gen<'b>
,因此整个表达式的结果变为Gen<Gen<Alphanumeric list>>
。这就是Gen<Alphanumeric list>
在下一个map
中显示为输入的原因。全部由设计。
您真正想要的操作通常称为bind
。这样的功能将具有签名(f: 'a -> Gen<'b>) -> Gen<'a> -> Gen<'b>
。也就是说,它将使用一个函数来生成另一个Gen
,而不是裸值。
不幸的是,由于某种原因,Gen
并没有公开bind
。它可以作为 gen
computation expression builder的一部分或作为operator >>=
(代表bind
的事实上的标准运算符)使用。
根据以上说明,您可以重新定义您的定义,如下所示:
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))
您也可以考虑使用计算表达式来构建生成器。不幸的是,没有为
where
表达式构建器定义gen
,因此您仍然必须使用suchThat
进行过滤。但是幸运的是,有一个特殊的函数Gen.choose
可以产生给定范围内的值: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)
}