如何将FsCheck生成器列表累积为单个值?

时间:2017-11-02 20:01:11

标签: f# fscheck property-based-testing

我写了一个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的方法,但我无法弄清楚如何,因为我不明白{{1}在引擎盖下工作(尚未)。我正在考虑类似以下的内容:

let!

我是否在正确的轨道上? let! text = pattern |> Seq.map toGen |> Seq.fold (?) (Gen.constant "") 的累加器和种子应该是什么样的?

1 个答案:

答案 0 :(得分:3)

你可以在这里使用fold之类的直觉是正确的,但问题是你需要一个fold版本,其中折叠函数返回Gen<'T>计算 - 所以来自F#的普通List.fold将无效。在这种情况下,我认为使用变异是完全正常的 - 你的代码看起来很清楚。

查看Gen模块中的功能,我看不到fold的版本,但我认为Gen.sequence可让您完成所需的操作:

let! textParts = Gen.sequence (Seq.map toGen pattern)
let text = String.concat "" textParts

Gen.sequence函数获取生成器列表并返回使用这些生成器生成值列表的生成器 - 这样,您可以一次生成所有文本部分,然后只是连接结果。

如果您想编写自己的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 }) ""

我没有对此进行测试,因此可能存在错误(最有可能的是,它会以错误的方式折叠,因此您最终会使用反向字符串),但一般结构应该是正确的。