【问题标题】:How to recursively use FsCheck generators?如何递归使用 FsCheck 生成器?
【发布时间】:2016-07-19 23:22:06
【问题描述】:

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

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。那么在不同级别组合生成器的正确方法是什么?

【问题讨论】:

  • 不能用System.Random,不能用Gen.choose吗?
  • @MarkSeemann,您可以通过立即采样使用choose 而不是Random,但这会破坏目的,因为这个choose 不会成为生成的生成器的一部分,但是将作为一种效用函数工作。真的不比Random 好。
  • @FyodorSoikin 我不同意——你永远不想在生成器中使用System.Random,因为它会破坏可重复性(即相同的种子每次返回不同的结果)并使收缩变得不可能(或至少是不确定的) .
  • @KurtSchelfthout:Mark 建议使用 Gen.choose 而不是 Random。如果在 OP 的第一个代码块中将其插入 Random 的位置,那将毫无用处,不是吗?
  • @FyodorSoikin 再次阅读您的评论 - 是的,如果您使用 Gen.choose |&gt; Gen.sample,它并不比 Random 更好。但不要认为这就是@MarkSeemann 的意思:) 到处都是混乱……但我认为我们都在说同样的话。

标签: f# fscheck


【解决方案1】:

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

但是您提供的功能在您的map call是,实际上,int -&gt; Gen&lt;Alphanumeric list&gt; - 也就是说,它返回了一些'b,但更具体地说,特别是Gen&lt;'b&gt;,所以整体表达的结果成为Gen&lt;Gen&lt;Alphanumeric list&gt;&gt;。这就是为什么Gen&lt;Alphanumeric list&gt;显示为下一个map的输入。所有设计。

你真正想要的操作通常被称为bind。此类功能将具有签名(f: 'a -&gt; Gen&lt;'b&gt;) -&gt; Gen&lt;'a&gt; -&gt; Gen&lt;'b&gt;。也就是说,它将采用一个函数,在987654338 @,而不是裸价值。

不幸的是,出于某种原因,Gen不公开bind。它可以作为gen computation expression builder或AS operator &gt;&gt;=(是代表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))

您还可以考虑使用计算表达式来构建生成器。不幸的是,没有为gen 987654346 @表达式构建器定义,因此您仍然必须使用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)
  }

【讨论】:

  • 非常感谢一个优秀的解释,fyodor!然而,我不明白Gen.Choose在他答案中使用的那个标志。你说它不会成为所产生的发电机的一部分,但对我而言,它看起来是(请参阅Mark的代码与Gen计算表达式)。 span>
  • mark误解了我的评论。他建议不仅仅使用Gen.choose,但使用它而不是随机 i>。如果您使用Gen.choose 987654351替换在第一个代码块中的使用随机使用,您需要立即对其进行采样,这将使它无用。但如果你重构了发电机,那么Gen.choose是正确的绑定,那么它当然不是没用的。 span>
  • 我很抱歉,我也误解了你的评论。我以为你根本不支持Gen.choose,我和我发现它相当令人信服,所以我接受了马克的答案。现在我得到了整个画面,当然你是第一个主要的命题。 span>
【解决方案2】:

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 值。

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

【讨论】:

  • 非常感谢!顺便说一句,你的 Pluralsight 课程是我最终开始在我的项目中使用 FsCheck 的转折点,这让我想到了我的问题,所以你也回答了它感觉是对的 :-) 你的代码看起来正是我所追求的(我忽略了解决方案中必不可少的 gen 计算表达式)。我很困惑为什么 Fyodor 不喜欢使用 Gen.choose,我需要更好地理解它。
  • 而说到非空字符串,不就是把Gen.listOf换成Gen.nonEmptyListOf吗?
  • @VagifAbilov 是的,如果您只想要非空字符串,您也可以使用 Gen.nonEmptyListOf。请注意,长度也可以增加到大于 10 的数字。
  • 哦对了,那么size可以大于10。
  • @MarkSeeman:这完全让我感到困惑,您没有注意到您的代码几乎是我比您早 13 小时在回答中给出的代码。太棒了!
猜你喜欢
  • 1970-01-01
  • 2012-10-05
  • 2019-10-18
  • 1970-01-01
  • 2015-01-19
  • 1970-01-01
  • 1970-01-01
  • 2020-01-14
  • 1970-01-01
相关资源
最近更新 更多