如何将 FsCheck 生成器列表累积为单个值?
How to accumulate a list of FsCheck generators into a single value?
我编写了一个 FsCheck 生成器,它生成随机的 glob 语法模式(例如 a*c?
)以及匹配该模式的随机字符串(例如 abcd
)。但是我的解决方案使用了一个可变变量,我对此感到很惭愧。看看:
open FsCheck
type TestData = {Pattern: string; Text: string}
let stringFrom alphabet =
alphabet |> Gen.elements |> Gen.listOf |> Gen.map (List.map string >> List.fold (+) "")
let singleCharStringFrom alphabet =
alphabet |> Gen.elements |> Gen.map string
let matchingTextAndPatternCombo = gen {
let toGen = function
| '*' -> stringFrom ['a'..'f']
| '?' -> singleCharStringFrom ['a'..'f']
| c -> c |> string |> Gen.constant
let! pattern = stringFrom (['a'..'c']@['?'; '*'])
let mutable text = ""
for gen in Seq.map toGen pattern do
let! textPart = gen
text <- text + textPart
return {Pattern = pattern; Text = text}
}
请注意 text
是可变的,以及它的值是如何在循环中累积的。
我的直觉告诉我,一定有办法将生成器 fold
变成 text
,但我不知道怎么做,因为我不明白 let!
在引擎盖下工作(还)。我正在考虑类似于以下内容的内容:
let! text = pattern |> Seq.map toGen |> Seq.fold (?) (Gen.constant "")
我走在正确的轨道上吗? fold
的累加器和种子应该是什么样的?
你的直觉认为 fold
之类的东西可以在这里使用是正确的,但问题是你需要 fold
的版本,其中折叠功能 returns Gen<'T>
计算 - 因此 F# 中的正常 List.fold
将不起作用。在这种情况下,我认为使用突变非常好 - 你的代码对我来说看起来很清楚。
查看 Gen
模块中的函数,我没有看到 fold
的版本,但我认为 Gen.sequence
可以很好地满足您的需求:
let! textParts = Gen.sequence (Seq.map toGen pattern)
let text = String.concat "" textParts
Gen.sequence
函数采用生成器列表和 returns 使用这些生成器生成值列表的生成器 - 这样,您可以一次生成所有文本部分,然后将它们连接起来结果。
如果您想编写自己的 fold
并使用它,它看起来会有些东西
像这样:
let rec fold f init xs = gen {
match xs with
| [] -> return init
| x::xs ->
let! state = f init x
return! fold f state xs }
折叠生成器的代码将是:
let! text =
Seq.map toGen pattern |> List.ofSeq |> fold (fun (text:string) g -> gen {
let! textPart = g
return text + textPart }) ""
我没有测试过这个,所以可能有错误(很可能,它折叠的方向不对,所以你最终会得到颠倒的字符串),但总体结构应该是正确的。
我编写了一个 FsCheck 生成器,它生成随机的 glob 语法模式(例如 a*c?
)以及匹配该模式的随机字符串(例如 abcd
)。但是我的解决方案使用了一个可变变量,我对此感到很惭愧。看看:
open FsCheck
type TestData = {Pattern: string; Text: string}
let stringFrom alphabet =
alphabet |> Gen.elements |> Gen.listOf |> Gen.map (List.map string >> List.fold (+) "")
let singleCharStringFrom alphabet =
alphabet |> Gen.elements |> Gen.map string
let matchingTextAndPatternCombo = gen {
let toGen = function
| '*' -> stringFrom ['a'..'f']
| '?' -> singleCharStringFrom ['a'..'f']
| c -> c |> string |> Gen.constant
let! pattern = stringFrom (['a'..'c']@['?'; '*'])
let mutable text = ""
for gen in Seq.map toGen pattern do
let! textPart = gen
text <- text + textPart
return {Pattern = pattern; Text = text}
}
请注意 text
是可变的,以及它的值是如何在循环中累积的。
我的直觉告诉我,一定有办法将生成器 fold
变成 text
,但我不知道怎么做,因为我不明白 let!
在引擎盖下工作(还)。我正在考虑类似于以下内容的内容:
let! text = pattern |> Seq.map toGen |> Seq.fold (?) (Gen.constant "")
我走在正确的轨道上吗? fold
的累加器和种子应该是什么样的?
你的直觉认为 fold
之类的东西可以在这里使用是正确的,但问题是你需要 fold
的版本,其中折叠功能 returns Gen<'T>
计算 - 因此 F# 中的正常 List.fold
将不起作用。在这种情况下,我认为使用突变非常好 - 你的代码对我来说看起来很清楚。
查看 Gen
模块中的函数,我没有看到 fold
的版本,但我认为 Gen.sequence
可以很好地满足您的需求:
let! textParts = Gen.sequence (Seq.map toGen pattern)
let text = String.concat "" textParts
Gen.sequence
函数采用生成器列表和 returns 使用这些生成器生成值列表的生成器 - 这样,您可以一次生成所有文本部分,然后将它们连接起来结果。
如果您想编写自己的 fold
并使用它,它看起来会有些东西
像这样:
let rec fold f init xs = gen {
match xs with
| [] -> return init
| x::xs ->
let! state = f init x
return! fold f state xs }
折叠生成器的代码将是:
let! text =
Seq.map toGen pattern |> List.ofSeq |> fold (fun (text:string) g -> gen {
let! textPart = g
return text + textPart }) ""
我没有测试过这个,所以可能有错误(很可能,它折叠的方向不对,所以你最终会得到颠倒的字符串),但总体结构应该是正确的。